diff --git a/.trinity/seals/widgets_widgets_catalog.json b/.trinity/seals/widgets_widgets_catalog.json new file mode 100644 index 0000000000..7a1af4a7b7 --- /dev/null +++ b/.trinity/seals/widgets_widgets_catalog.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+24a3abcad", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:9e9a74d4e74e8bd4639440f35b7307365649c1324547f2e2b9c8704e34f297d2", + "gen_hash_rust": "sha256:048ce118f452baf633cb8a13f8cbdd1e75a5c4142ecbd71f5192a54b918fba91", + "gen_hash_verilog": "sha256:489cbe16df61ce73e8aeea5b3ff7758c0f8b01f24d5ac289c16dd39bbd106e2b", + "gen_hash_zig": "sha256:c5583b8688bb1e39f75939d8c34fe4b916c0c111b7f48c1f788949b7c78bb97c", + "module": "widgets_catalog", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-20T13:41:56Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:f80de9a04c020e9a4c39744fd7e58d6e1fa8a180bda2f7b80454497fc77a0d87", + "spec_path": "specs/widgets/catalog.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/specs/widgets/catalog.t27 b/specs/widgets/catalog.t27 index 93014467bb..356a9d0ebe 100644 --- a/specs/widgets/catalog.t27 +++ b/specs/widgets/catalog.t27 @@ -37,7 +37,7 @@ ; 14667449, lut-explain 1395124, bit-diff 275620, vcd-wrapped 76076, pin-map 180587, synth-smells 87094, fsm-sketch 91657 (its Math.sin/cos/atan2 stay with the host: V8 differs from libm in the last bit), bit-autopsy 96007. ; With these, all 12 pure logic files of the census (2775 lines of JavaScript) exist as .t27. ; gatle is at PORT_LOGIC (specs/widgets/gatle_rule.t27: 866178 comparisons vs rule.js, 0 mismatches) -; and jtag-verdict, race-the-bee, t27-vs-nvfp4, t27-cell, build-receipt, uart-bucket, test-touch, pin-map, lut-depth, lut-explain, gf16-calc, resource-bars and fits-on (the first canvas page: its DOM and its canvas call sequence both compared) fsm-sketch, cost-curves, lut-treemap, stat-card, synth-smells, vcd-wrapped, fasm-skyline, bit-diff and bit-autopsy at PORT_DRAW (specs/widgets/jtag_verdict.t27: 3015 DOM +; and jtag-verdict, race-the-bee, t27-vs-nvfp4, t27-cell, build-receipt, uart-bucket, test-touch, pin-map, lut-depth, lut-explain, gf16-calc, resource-bars and fits-on (the first canvas page: its DOM and its canvas call sequence both compared) fsm-sketch, cost-curves, lut-treemap, stat-card, synth-smells, vcd-wrapped, fasm-skyline, bit-diff, bit-autopsy, idcode, slack-waterfall, test-waves, placement-map, gatle and t27c-stages at PORT_DRAW (all 28) (specs/widgets/jtag_verdict.t27: 3015 DOM ; comparisons vs tool.js, 0 mismatches; specs/widgets/race_the_bee.t27: 59, with 13 data variants, 0). Every other tool is at PORT_WORDS: its words live in its spec, its logic and drawing ; still live in JavaScript. A stage is reached on the t27 side first; the page in trinity keeps ; loading the hand-written file until that repo's migration PR deletes it. A tool moves up only when @@ -91,7 +91,7 @@ pub const PORT_LOGIC : u8 = 2; // pure logic is .t27, generated to JS by gen-ts pub const PORT_DRAW : u8 = 3; // the page is drawn by the trusted renderer from a plan pub const PORT_SEALED : u8 = 4; // the spec seal keys the page, the data and the card pub const PORT_STAGES : u8 = 5; -pub const TOOL_PORT : [28]u8 = [3, 3, 3, 3, 3, 3, 3, 2, 3, 1, 3, 3, 3, 3, 3, 1, 3, 3, 1, 3, 3, 3, 3, 1, 3, 1, 3, 3]; +pub const TOOL_PORT : [28]u8 = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]; ; Port waves, by the hand lines a tool still carries (logic + draw + css + data + card). ; Wave 1 is the small tools, wave 3 the largest; a wave is a batch of port issues. @@ -227,12 +227,11 @@ test the_waves_cover_every_tool_once { assert hand_lines(12) == 1432; } -test twenty_two_pages_are_native_and_gatle_alone_keeps_a_logic_stage { - assert count_port(PORT_DRAW) == 22; - assert count_port(PORT_LOGIC) == 1; - assert count_port(PORT_WORDS) == 5; +test every_tool_has_a_native_page { + assert count_port(PORT_DRAW) == 28; + assert count_port(PORT_LOGIC) == 0; + assert count_port(PORT_WORDS) == 0; assert count_port(PORT_SEALED) == 0; - assert TOOL_PORT[0] == PORT_DRAW; - assert TOOL_PORT[1] == PORT_DRAW; - assert TOOL_PORT[7] == PORT_LOGIC; + assert TOOL_PORT[7] == PORT_DRAW; + assert TOOL_PORT[23] == PORT_DRAW; } diff --git a/specs/widgets/gatle.t27 b/specs/widgets/gatle.t27 new file mode 100644 index 0000000000..29bc7fba2e --- /dev/null +++ b/specs/widgets/gatle.t27 @@ -0,0 +1,2494 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/gatle.t27 -- Gatle: guess how many LUTs yosys spends on today's t27 module +; Source of truth for public/widgets/gatle/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English of its own: every +; word on screen is a SAY_ constant here, and the rules are K_ constants. +; ASCII only (L3), English only (LANG-EN). + +; WHAT IT SHOWS: a daily guessing game in the manner of Wordle. Each UTC day every reader gets the +; same real module from the vendored specs (public/t27/files/specs), its t27 source, and the +; Verilog that public/t27/t27_compiler.wasm emits for it -- compiled in the reader's browser, with +; the sha256 compared against the Verilog yosys read. The reader has K_GUESSES guesses at how many +; LUTs (LUT1 to LUT6 cells) yosys spends on it. Each guess is graded: close when within +; K_CLOSE_PCT percent of the truth (the boundary counts as close), warm within K_WARM_PCT, cold +; otherwise, with an arrow toward the truth. The end shows flip-flops, CARRY4, MUXF7/8, DSP48E1 and +; every cell type, the same module synthesised the other way (DSPs allowed), and a share text of +; emoji rows, "Gatle #N" and K_SHARE_URL, copied by a button and posted by nobody but the reader. +; Practice mode deals a random module from the same pool. + +; THE DAY: the puzzle of a UTC day is (days since 1970-01-01 minus K_EPOCH_DAY) modulo +; K_POOL_SIZE, an index into K_POOL_IDS; the epoch day is Gatle #1. The pool order is the sha256 +; of each spec path, a fixed shuffle nobody chose. public/widgets/gatle/rule.js holds these rules +; as code; scripts/widget-data/gatle.mjs imports that file and checks it against the tests below. + +; THE NUMBERS: scripts/widget-data/gatle.mjs compiles every vendored spec to Verilog with the wasm +; compiler in node, keeps modules with data ports, and runs yosys on each one twice, with and +; without DSPs. The answer is the K_SYNTH run, -nodsp, so the number to guess is LUT fabric only and +; can never depend on whether yosys chose a DSP48E1. In this pool the run with DSPs allowed placed +; none anyway; its LUT count differs by a few on some modules, and the page shows it at the end. A +; module is in the pool when yosys accepts it within ten minutes, its code (lines neither blank nor +; comment-only) fits K_MAX_CODE_LINES by K_MAX_CODE_COLS, and it costs K_MIN_LUTS to K_MAX_LUTS +; LUTs. Every other spec is listed in pool.json with its reason. The script exits non-zero when +; this file and pool.json disagree. + +; WHAT IT DOES NOT CLAIM: LUT counts after synthesis are not a placed-and-routed design; no timing, +; no board, no Vivado. The modules are mostly combinational as the emitter writes them +; (K_POOL_ZERO_FF of K_POOL_SIZE have no flip-flop), so the guess is LUTs and flip-flops are shown +; at the end rather than guessed. There are no streaks and no statistics: the page keeps nothing, +; no storage and no cookies, and a day's guesses live only in the address after the #. Most +; sources are longer than one phone screen: under 40 code lines the pool would hold too few +; modules for a daily game, so the code-size rule is a scrolling box, comment lines folded. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_gatle; + +use widgets::html_out::ok; +use widgets::html_out::OVER; +use widgets::html_out::put; +use widgets::html_out::raw; +use widgets::html_out::put_escaped; +use widgets::html_out::text; +use widgets::html_out::num; +use widgets::html_out::attr; +use widgets::html_out::fill; +use widgets::html_out::fill_named; +use widgets::html_out::dec_to; +use widgets::html_out::name_is; +use widgets::html_out::is_word_byte; +use widgets::html_out::H_LT; +use widgets::html_out::H_GT; +use widgets::html_out::H_AMP; +use widgets::html_out::H_QUOTE; +use widgets::html_out::H_APOS; +use widgets::html_out::H_SPACE; +use widgets::html_out::H_SLASH; +use widgets::html_out::H_EQ; +use widgets::html_out::num_grouped; +use widgets::json_scan::NO_SPAN; +use widgets::json_scan::SPAN; +use widgets::json_scan::J_NONE; +use widgets::json_scan::J_OBJECT; +use widgets::json_scan::J_ARRAY; +use widgets::json_scan::J_STRING; +use widgets::json_scan::J_NUMBER; +use widgets::json_scan::J_TRUE; +use widgets::json_scan::J_FALSE; +use widgets::json_scan::J_NULL; +use widgets::json_scan::jspan; +use widgets::json_scan::span_lo; +use widgets::json_scan::span_hi; +use widgets::json_scan::skip_ws; +use widgets::json_scan::string_end; +use widgets::json_scan::value_end; +use widgets::json_scan::root; +use widgets::json_scan::kind; +use widgets::json_scan::key_is; +use widgets::json_scan::obj_get; +use widgets::json_scan::arr_len; +use widgets::json_scan::arr_at; +use widgets::json_scan::hex4; +use widgets::json_scan::escape_at; +use widgets::json_scan::cp_utf8_len; +use widgets::json_scan::cp_utf8_byte; +use widgets::json_scan::str_len; +use widgets::json_scan::str_byte; +use widgets::js_num::JN_OVER; +use widgets::js_num::JN_CAP; +use widgets::js_num::JN_TWO52; +use widgets::js_num::JN_TWO53; +use widgets::js_num::jn_put; +use widgets::js_num::jn_raw; +use widgets::js_num::jn_digits; +use widgets::js_num::jn_mul; +use widgets::js_num::jn_mul2; +use widgets::js_num::jn_mul5; +use widgets::js_num::jn_shift; +use widgets::js_num::jn_cmp; +use widgets::js_num::jn_expo; +use widgets::js_num::jn_mant; +use widgets::js_num::jn_make; +use widgets::js_num::jn_exact; +use widgets::js_num::jn_fscale; +use widgets::js_num::js_is_nan; +use widgets::js_num::js_nan; +use widgets::js_num::js_inf; +use widgets::js_num::js_floor; +use widgets::js_num::js_ceil; +use widgets::js_num::js_max; +use widgets::js_num::js_min; +use widgets::js_num::js_round; +use widgets::js_num::jn_shortest; +use widgets::js_num::jn_copy; +use widgets::js_num::jn_put_run; +use widgets::js_num::jn_layout; +use widgets::js_num::js_str_num; +use widgets::js_num::js_to_fixed; +use widgets::js_num::js_locale_num; +use widgets::js_num::jn_num_ok; +use widgets::js_num::jn_cmp_text; +use widgets::js_num::js_parse_num; +use widgets::js_num::jn_dec_value; +use widgets::js_num::jn_abs; +use widgets::js_num::js_str_u64; +use widgets::json_scan::is_true; +use widgets::json_scan::is_false; +use widgets::json_scan::is_null; +use widgets::json_scan::num_f64; +use widgets::json_scan::num_i64; +use widgets::js_val::jv_valid; +use widgets::js_val::jv_string_ok; +use widgets::js_val::jv_number_ok; +use widgets::js_val::jv_lit_ok; +use widgets::js_val::jv_is_ws; +use widgets::js_val::jv_ws_at; +use widgets::js_val::jv_ws_before; +use widgets::sha256_hex::SH_K; +use widgets::sha256_hex::SH_M32; +use widgets::sha256_hex::sh_rotr; +use widgets::sha256_hex::sh_hexd; +use widgets::sha256_hex::sh_block; +use widgets::sha256_hex::sha256_hex; +use widgets::gatle_rule::DAY_MS; +use widgets::gatle_rule::HEAT_CLOSE; +use widgets::gatle_rule::HEAT_WARM; +use widgets::gatle_rule::HEAT_COLD; +use widgets::gatle_rule::DIR_HIT; +use widgets::gatle_rule::DIR_UP; +use widgets::gatle_rule::DIR_DOWN; +use widgets::gatle_rule::EMOJI_HEAT; +use widgets::gatle_rule::ARROWS; +use widgets::gatle_rule::day_number; +use widgets::gatle_rule::day_index; +use widgets::gatle_rule::puzzle_number; +use widgets::gatle_rule::off100; +use widgets::gatle_rule::heat; +use widgets::gatle_rule::dir; +use widgets::gatle_rule::heat_code; +use widgets::gatle_rule::arrow_code; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "gatle"; +pub const TITLE : str = "Gatle: guess how many LUTs yosys spends on today's t27 module"; +pub const DESCRIPTION : str = "A daily guessing game: one real t27 module, its Verilog compiled in your browser, six guesses at the LUT count yosys reported for it."; +pub const IMAGE_ALT : str = "Gatle card, a sample game of puzzle #1 (2026-10-04), ternary/gft_sgd_step: guesses 122, 607 and 1351 are told more, 2014 is told fewer, and 1650 lands within 10% of the 1826 LUTs yosys reported with synth_xilinx -nodsp. One of 37 real t27 modules, one a day."; +pub const HOOK : str = "Can you out-guess yosys? One real t27 module a day, six guesses at its LUT count. Puzzle #1 cost 1826 LUTs; splitting the range took five."; +pub const CATEGORY : str = "game"; +pub const DATA_SOURCES : [4]str = ["public/t27/t27_compiler.wasm (sha256 bb39b9a5ca41...), run in node by scripts/widget-data/gatle.mjs on 2026-10-04: the Verilog of every vendored spec; the page recompiles the day's module in the browser and compares sha256", "Yosys 0.67+post (git sha1 b8e7da6f), run 2026-10-04: read_verilog -sv; synth_xilinx -family xc7 -flatten -nodsp -top ; stat -json. The LUT1 to LUT6 cells are the answer", "The same Yosys in the same run: synth_xilinx -family xc7 -flatten, DSPs allowed, shown beside the answer when a game ends", "scripts/widget-data/gatle.mjs: public/widgets/gatle/pool.json with every rejected spec and its reason; --refilter tightened the code-size rule to the spec's, with no new yosys run"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +; --- What is measured and what is kept -------------------------------------------------------- +pub const K_SYNTH : str = "synth_xilinx -family xc7 -flatten -nodsp"; +pub const K_OTHER_SYNTH : str = "synth_xilinx -family xc7 -flatten"; +pub const K_MIN_LUTS : u16 = 5; +pub const K_MAX_LUTS : u16 = 3000; +pub const K_MAX_CODE_LINES : u16 = 160; +pub const K_MAX_CODE_COLS : u16 = 120; +pub const K_DATA_FILE : str = "./pool.json"; + +; --- The pool: written from pool.json by `node scripts/widget-data/gatle.mjs --spec-lines` ------ +pub const K_POOL_SIZE : u8 = 37; +pub const K_POOL_IDS : [37]str = ["ternary/gft_sgd_step", "ternary/gft_mlp2", "igla/race/ternary_lut_table", "ternary/comb_ternary_dot", "igla/race/ternary_mac_group", "ternary/gft_recip", "ternary/gft_dot4_rne", "ternary/stream_ternary_mac", "ternary/ternary_mac", "ternary/gft_classify", "ternary/gft_mul_rne", "numeric/e8m0", "ternary/gft_dot2_rne", "boards/wukong_v1", "ternary/gft_layer2", "igla/race/phi_accumulator_growth", "ternary/comb_bitnet_neuron", "igla/race/ternary_node", "ternary/gft_exp2", "ternary/gft_dot2", "ternary/gft_relu", "ternary/ternary_xor", "ternary/gft_smul", "igla/race/unit_weights_node", "ternary/comb_bitnet_layer", "ternary/gft_add_rne", "compiler/mod_structure", "ternary/gft_argmax4", "ternary/ternary_ripple_adder", "ternary/ternary_full_adder", "ternary/gft_signed_mac", "igla/race/mvp_ternary_classifier", "ternary/gft_madd", "ternary/gft_sadd", "ternary/activation_quantizer", "ternary/gft_dot8", "ternary/gft_dot4"]; +pub const K_POOL_ZERO_FF : u8 = 36; +pub const K_CANDIDATES : u16 = 88; +pub const K_CORPUS_SPECS : u16 = 1682; + +; --- The day ---------------------------------------------------------------------------------- +; 2026-10-04, the UTC day the pool was built, is day 20730 since 1970-01-01, and Gatle #1. +pub const K_EPOCH_DATE : str = "2026-10-04"; +pub const K_EPOCH_DAY : u32 = 20730; +; Two fixed dates with their day numbers, pool indexes and puzzle numbers. gatle.mjs checks that +; rule.js gives exactly these; the test below checks the arithmetic against K_POOL_SIZE. +pub const K_TEST_DATES : [2]str = ["2026-10-04", "2027-01-01"]; +pub const K_TEST_DAYS : [2]u32 = [20730, 20819]; +pub const K_TEST_INDEX : [2]u32 = [0, 15]; +pub const K_TEST_NUMBER : [2]u32 = [1, 90]; + +; --- The guess -------------------------------------------------------------------------------- +pub const K_GUESSES : u8 = 6; +pub const K_CLOSE_PCT : u8 = 10; +pub const K_WARM_PCT : u8 = 50; +pub const K_MAX_GUESS : u32 = 99999; +; The 10% boundary against a truth of 200: 180 and 220 are close, 179 and 221 are not. +; |guess - truth| * 100 <= truth * K_CLOSE_PCT is close; integer arithmetic, no rounding. +pub const K_RULE_TRUTH : u16 = 200; +pub const K_RULE_GUESSES : [4]u16 = [180, 179, 220, 221]; +pub const K_RULE_HEAT : [4]str = ["close", "warm", "close", "warm"]; +pub const K_RULE_DIR : [4]str = ["hit", "up", "hit", "down"]; + +; --- The share text --------------------------------------------------------------------------- +; Code points: green, yellow and red squares for close, warm and cold (the tile colours); up and down arrows, +; each followed by the emoji presentation selector. +pub const K_EMOJI_HEAT : [3]u32 = [129001, 129000, 128997]; +pub const K_ARROWS : [2]u32 = [11014, 11015]; +pub const K_EMOJI_SELECTOR : u32 = 65039; +pub const K_SHARE_URL : str = "t27.ai/widgets/gatle/"; + +; --- Words ------------------------------------------------------------------------------------ +; {name} is filled by tool.js from pool.json or from the K_ constants above; nothing else is. +pub const SAY_LOADING : str = "Loading..."; +pub const SAY_LOAD_FAILED : str = "The puzzle pool could not be read."; +pub const SAY_MODES : [2]str = ["Today", "Practice"]; +pub const SAY_DAILY_LINE : str = "Gatle #{n}, {date} UTC: puzzle {i} of {size}. Every reader gets this module today."; +pub const SAY_PAST_LINE : str = "Gatle #{n} of {date}, opened from a link. Today is #{today}."; +pub const SAY_PRACTICE_LINE : str = "Practice: a random module from the {size}. Today's game is untouched."; +pub const SAY_NEW_PRACTICE : str = "Another module"; +pub const SAY_TABS : [2]str = ["t27 source", "Verilog"]; +pub const SAY_MODULE_META : str = "top module {top}; {code} code lines, {lines} with comments"; +pub const SAY_SOURCE_FAILED : str = "The module's source could not be read."; +pub const SAY_SHOW_COMMENTS : str = "Show {n} comment lines"; +pub const SAY_HIDE_COMMENTS : str = "Hide comment lines"; +pub const SAY_SOURCE_DRIFT : str = "This source is not the one yosys read: its sha256 differs from pool.json."; +pub const SAY_COMPILING : str = "Compiling in your browser with t27_compiler.wasm..."; +pub const SAY_COMPILE_FAILED : str = "The compiler could not run in this browser."; +pub const SAY_VERILOG_SAME : str = "Compiled here in {ms} ms: {bytes} bytes, sha256 {sha}..., the same Verilog yosys read."; +pub const SAY_VERILOG_DRIFT : str = "Compiled here in {ms} ms: sha256 {sha}... is not {was}..., the Verilog yosys read."; +pub const SAY_VERILOG_UNCHECKED : str = "Compiled here in {ms} ms: {bytes} bytes. This browser cannot hash it to compare."; +pub const SAY_QUESTION : str = "How many LUTs does yosys spend on this module?"; +pub const SAY_RULE_NOTE : str = "LUT1 to LUT6 cells after {synth}, {version}. Close: within {close}%. Warm: within {warm}%."; +pub const SAY_BOARD_LABEL : str = "Your guesses"; +pub const SAY_HEAT : [3]str = ["close, within {close}%", "warm, within {warm}%", "cold"]; +pub const SAY_DIRS : [3]str = ["got it", "more", "fewer"]; +pub const SAY_INPUT_LABEL : str = "Your guess, in LUTs"; +pub const SAY_GUESS : str = "Guess"; +pub const SAY_GUESSES_LEFT : str = "{n} of {max} guesses left."; +pub const SAY_BAD_GUESS : str = "A whole number from 0 to {max}, please."; +pub const SAY_ALREADY : str = "You guessed that number already."; +pub const SAY_WON : str = "Within {close}% in {n} of {max}."; +pub const SAY_LOST : str = "Out of guesses."; +pub const SAY_ANSWER : str = "yosys: {lut} LUTs"; +pub const SAY_REVEAL : str = "Also {ff} flip-flops, {carry4} CARRY4, {muxf7} MUXF7, {muxf8} MUXF8, {dsp} DSP48E1; {cells} cells in all."; +pub const SAY_OTHER_SAME : str = "With {mode}: the same LUT and DSP counts."; +pub const SAY_OTHER_DIFF : str = "With {mode}: {lut} LUTs and {dsp} DSP48E1."; +pub const SAY_BY_TYPE : str = "Every cell type yosys reported"; +pub const SAY_SHARE_LABEL : str = "Share text"; +pub const SAY_SHARE_HEAD : str = "Gatle #{n} {score}/{max}"; +pub const SAY_SHARE_PRACTICE : str = "Gatle practice {score}/{max}"; +pub const SAY_SHARE_LOST : str = "X"; +pub const SAY_COPY : str = "Copy"; +pub const SAY_COPIED : str = "Copied."; +pub const SAY_COPY_FAILED : str = "Copy failed; the text is selected, copy it by hand."; +pub const SAY_NO_STREAKS : str = "No streaks and no stats: this page keeps nothing. Your guesses live only in the address after the #, so a bookmark or a link carries them and nothing else does."; +pub const SAY_POOL_NOTE : str = "{size} puzzles: the modules of {specs} vendored specs that emit Verilog with data ports ({candidates} synthesised), fit {lines} code lines by {cols} columns and cost {min} to {max} LUTs; {zeroff} have no flip-flop. Built {date}."; + +test day_index_on_two_fixed_dates { + assert K_TEST_INDEX[0] == (K_TEST_DAYS[0] - K_EPOCH_DAY) % K_POOL_SIZE; + assert K_TEST_INDEX[1] == (K_TEST_DAYS[1] - K_EPOCH_DAY) % K_POOL_SIZE; + assert K_TEST_NUMBER[0] == K_TEST_DAYS[0] - K_EPOCH_DAY + 1; + assert K_TEST_NUMBER[1] == K_TEST_DAYS[1] - K_EPOCH_DAY + 1; + assert K_TEST_DAYS[0] == K_EPOCH_DAY; +} + +test close_is_within_ten_percent_boundary_included { + assert K_CLOSE_PCT == 10; + assert K_RULE_TRUTH - K_RULE_GUESSES[0] == 20; + assert K_RULE_TRUTH * K_CLOSE_PCT >= (K_RULE_TRUTH - K_RULE_GUESSES[0]) * 100; + assert K_RULE_TRUTH - K_RULE_GUESSES[1] == 21; + assert K_RULE_TRUTH * K_CLOSE_PCT < (K_RULE_TRUTH - K_RULE_GUESSES[1]) * 100; + assert K_RULE_TRUTH * K_WARM_PCT >= (K_RULE_TRUTH - K_RULE_GUESSES[1]) * 100; + assert K_RULE_GUESSES[2] - K_RULE_TRUTH == 20; + assert K_RULE_TRUTH * K_CLOSE_PCT >= (K_RULE_GUESSES[2] - K_RULE_TRUTH) * 100; + assert K_RULE_GUESSES[3] - K_RULE_TRUTH == 21; + assert K_RULE_TRUTH * K_CLOSE_PCT < (K_RULE_GUESSES[3] - K_RULE_TRUTH) * 100; + assert K_RULE_HEAT[0] == "close"; + assert K_RULE_HEAT[1] == "warm"; + assert K_RULE_DIR[1] == "up"; + assert K_RULE_DIR[3] == "down"; +} + +test pool_size_matches_the_id_list { + assert K_POOL_IDS[K_POOL_SIZE - 1] != ""; + assert K_POOL_ZERO_FF <= K_POOL_SIZE; + assert K_POOL_SIZE <= K_CANDIDATES; + assert K_CANDIDATES <= K_CORPUS_SPECS; +} + +test sends_nothing_and_keeps_nothing { + assert SENDS_NOTHING == true; + assert READS_LOCAL_FILES == false; + assert K_DATA_FILE == "./pool.json"; + assert K_MIN_LUTS < K_MAX_LUTS; +} + + +; ================================================================================================= +; THE NATIVE PAGE (epic gHashTag/t27#10937, stage "page" of specs/t27f/widget_factory.t27) +; Everything below replaces public/widgets/gatle/tool.js (309 lines of hand-written JavaScript) and +; rule.js (parseHash and formatHash; the rest of rule.js is gatle_rule.t27, used here). The words and +; numbers above are unchanged. The host protocol is race_the_bee's (w_init, w_data, w_click, +; w_render) plus the services a day-by-day game needs: +; w_init(hash) the page opens; the game is made when the pool has been read +; w_data(i, bytes) pool.json (K_DATA_FILE); w_data_failed(i) when it could not be read +; w_hash(hash) the address's fragment changed (the game is read from it again) +; w_hash_out(dst) the fragment to show after an event that saved the game (0: leave it) +; w_input(i, text) the guess box changed (its value, which a number box blanks when it is no number) +; w_click(i) button i in page order: the heads (2 or 3), the 2 tabs, the comment toggle, the play button +; w_toggle(i, open) the details element was opened or closed by the reader +; w_value(i, dst) the text the guess box must hold now (0 bytes: empty it; 4294967295: leave it) +; w_wall(ms) Date.now() before each event; w_now(t) performance.now(); w_rand(r) Math.random() +; w_base(url) the page's directory, from which the source links are made +; w_effect(k, dst) effects after an event: 'f' + id + path = fetch a file and answer w_fetched(id, ok, bytes); +; 'F' + selector = focus; 'n' + selector = scroll into view; 'w' + selector, LF, text = copy +; the text (the old way on the element's text if refused) and answer w_copied(ok); +; 't' + id + decimal ms = call w_timer(id) after ms +; w_compile_need(dst) / w_compile_give(bytes, ok, t0, t1) +; the compiler service of t27c_stages.t27: the source to compile, the answer +; w_render(dst) the page's HTML, length (0 on overflow) +; The sha256 of the source and of the Verilog is sha256_hex.t27. State is the address's fragment only, +; as in tool.js. Not carried over from JavaScript: a pool file or a source above 1 MiB, more than 256 +; puzzles, a compiler answer above 1 MiB, a Verilog above 1 MiB or 512 KiB of source are taken for a failure; +; 256 games remembered in a visit (tool.js remembers all); a browser that cannot hash is not modelled (this page always +; hashes); invalid UTF-8 in a source is not replaced by U+FFFD; a pool file with a field missing +; stays at "Loading..." in tool.js (an exception) and draws blanks here; a number box value that no +; browser would hand over (it blanks what is no number) is not modelled beyond what the box gives. +; The browser's HTML parser drops a newline right after
 and turns a carriage return into a
+; newline: the page writes a second newline where the text starts with one and 
 for a carriage return.
+
+pub const W_DATA_CAP : u32 = 1048576;
+pub const W_POOL_MAX : u32 = 256;
+pub const W_SRC_TOTAL : u32 = 1048576;
+pub const W_RES_CAP : u32 = 1048576;
+pub const W_CODE_CAP : u32 = 1048576;
+pub const W_COMPILE_CAP : u32 = 524288;
+pub const W_MEM_MAX : u32 = 256;
+pub const W_COPY_MS : u32 = 2500;
+pub const W_DAY_MS : f64 = 86400000.0;
+pub const W_V_NONE : u32 = 0;
+pub const W_V_STR : u32 = 1;
+pub const W_V_RAW : u32 = 2;
+pub const W_V_NUM : u32 = 3;
+pub const W_V_SC : u32 = 4;
+pub const W_NAMES : str = "n date i size today top code lines ms bytes sha was synth version close warm max lut ff carry4 muxf7 muxf8 dsp cells mode score specs candidates zeroff min cols";
+
+pub var DATA : [1048576]u8 = [0; 1048576];
+pub var DATA_N : u32 = 0;
+pub var DATA_STATE : u32 = 0;
+pub var HASH_IN : [4096]u8 = [0; 4096];
+pub var HASH_IN_N : u32 = 0;
+pub var HASH_DIRTY : bool = false;
+pub var RAW_OUT : bool = false;
+pub var D_ROOT : i64 = -1;
+pub var D_POOL : i64 = -1;
+pub var D_YOSYS : i64 = -1;
+pub var D_CORPUS : i64 = -1;
+pub var D_DATE : i64 = -1;
+pub var POOL_SP : [256]i64 = [-1; 256];
+pub var POOL_N : u32 = 0;
+pub var ZEROFF : u32 = 0;
+pub var G_PRACTICE : bool = false;
+pub var G_DAY : u32 = 0;
+pub var G_INDEX : u32 = 0;
+pub var G_GUESS : [6]u32 = [0; 6];
+pub var G_N : u32 = 0;
+pub var MEM_KEY : [256]u32 = [0; 256];
+pub var MEM_GUESS : [1536]u32 = [0; 1536];
+pub var MEM_N : [256]u32 = [0; 256];
+pub var MEM_USED : u32 = 0;
+pub var MEM_NEXT : u32 = 0;
+pub var V_TAB : u32 = 0;
+pub var V_COMMENTS : bool = false;
+pub var HDR_T : u32 = 0;
+pub var WALL : f64 = 0.0;
+pub var NOW : f64 = 0.0;
+pub var RAND : f64 = 0.0;
+pub var BASE : [1024]u8 = [0; 1024];
+pub var BASE_N : u32 = 0;
+pub var MSG_KIND : u32 = 0;
+pub var CLEAR_INPUT : bool = false;
+pub var INPUT_TEXT : [256]u8 = [0; 256];
+pub var INPUT_N : u32 = 0;
+pub var DET_OPEN : bool = false;
+pub var SHARE_EPOCH : u32 = 0;
+pub var COPY_EPOCH : u32 = 0;
+pub var COPY_MSG : u32 = 0;
+pub var SRC_STATE : [256]u32 = [0; 256];
+pub var SRC_OFF : [256]u32 = [0; 256];
+pub var SRC_LEN : [256]u32 = [0; 256];
+pub var SRC_SHA : [16384]u8 = [0; 16384];
+pub var SRC_STORE : [1048576]u8 = [0; 1048576];
+pub var SRC_USED : u32 = 0;
+pub var VER_STATE : u32 = 0;
+pub var VER_PENDING : bool = false;
+pub var VER_T0 : f64 = 0.0;
+pub var VER_MS : f64 = 0.0;
+pub var VER_BYTES : u32 = 0;
+pub var VER_SAME : bool = false;
+pub var VER_SHA : [64]u8 = [0; 64];
+pub var CODE : [1048576]u8 = [0; 1048576];
+pub var CODE_N : u32 = 0;
+pub var RES : [1048576]u8 = [0; 1048576];
+pub var SC : [1024]u8 = [0; 1024];
+pub var SC_N : u32 = 0;
+pub var F_K : [40]u32 = [0; 40];
+pub var F_R : [40]i64 = [0; 40];
+pub var EQ_KIND : [8]u32 = [0; 8];
+pub var EQ_ARG : [8]u32 = [0; 8];
+pub var EQ_N : u32 = 0;
+pub var P_DAY : i64 = -1;
+pub var P_PRACTICE : i64 = -1;
+pub var P_G : [6]u32 = [0; 6];
+pub var P_GN : u32 = 0;
+pub var KV : [4096]u8 = [0; 4096];
+pub var BT_K : [64]i64 = [-1; 64];
+pub var BT_V : [64]i64 = [-1; 64];
+pub var SHA_H : [8]u32 = [0; 8];
+pub var SHA_W : [64]u32 = [0; 64];
+pub var SHA_O : [64]u8 = [0; 64];
+
+// --- writing ---------------------------------------------------------------------------------------
+
+fn gt_esc(o: []u8, n: u32, b: u8) -> u32 {
+    if (RAW_OUT) { return put(o, n, b); }
+    if (b == 13) { return raw(o, n, "
"); }
+    return put_escaped(o, n, b);
+}
+
+fn gt_text(o: []u8, n0: u32, s: []const u8) -> u32 {
+    var n : u32 = n0;
+    var i : usize = 0;
+    while (i < s.len) {
+        n = gt_esc(o, n, s[i]);
+        i = i + 1;
+    }
+    return n;
+}
+
+/// A JSON string value as HTML text.
+fn gt_jstr(o: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+    var n : u32 = n0;
+    if (kind(d, sp) != J_STRING) { return n; }
+    const hi : usize = span_hi(sp) - 1;
+    var p : usize = span_lo(sp) + 1;
+    while (p < hi) {
+        if (d[p] == 92 and p + 1 < hi) {
+            const e : u32 = escape_at(d, p + 1, hi);
+            const cp : u32 = e / 16;
+            const w : u32 = cp_utf8_len(cp);
+            var j : u32 = 0;
+            while (j < w) {
+                n = gt_esc(o, n, cp_utf8_byte(cp, j));
+                j = j + 1;
+            }
+            p = p + 1 + ((e % 16) as usize);
+        } else {
+            n = gt_esc(o, n, d[p]);
+            p = p + 1;
+        }
+    }
+    return n;
+}
+
+fn gt_jraw(o: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+    if (sp < 0) { return n0; }
+    var n : u32 = n0;
+    var q : usize = span_lo(sp);
+    const e : usize = span_hi(sp);
+    while (q < e) {
+        n = put(o, n, d[q]);
+        q = q + 1;
+    }
+    return n;
+}
+
+/// String(v) of a JSON value.
+fn gt_jstring(o: []u8, n: u32, d: []const u8, sp: i64) -> u32 {
+    const k : u8 = kind(d, sp);
+    if (k == J_STRING) { return gt_jstr(o, n, d, sp); }
+    if (k == J_NUMBER) { return gt_jraw(o, n, d, sp); }
+    if (k == J_TRUE) { return raw(o, n, "true"); }
+    if (k == J_FALSE) { return raw(o, n, "false"); }
+    if (k == J_NULL) { return raw(o, n, "null"); }
+    return raw(o, n, "undefined");
+}
+
+fn gt_d() -> []const u8 {
+    return DATA[0..(DATA_N as usize)];
+}
+
+/// The decoded string at sp copied to dst (cut at dst's length); its length.
+fn gt_jcopy(dst: []u8, d: []const u8, sp: i64) -> u32 {
+    var n : u32 = 0;
+    if (kind(d, sp) != J_STRING) { return 0; }
+    const hi : usize = span_hi(sp) - 1;
+    var p : usize = span_lo(sp) + 1;
+    while (p < hi) {
+        if (d[p] == 92 and p + 1 < hi) {
+            const e : u32 = escape_at(d, p + 1, hi);
+            const cp : u32 = e / 16;
+            const w : u32 = cp_utf8_len(cp);
+            var j : u32 = 0;
+            while (j < w) {
+                if ((n as usize) < dst.len) { dst[n as usize] = cp_utf8_byte(cp, j); n = n + 1; }
+                j = j + 1;
+            }
+            p = p + 1 + ((e % 16) as usize);
+        } else {
+            if ((n as usize) < dst.len) { dst[n as usize] = d[p]; n = n + 1; }
+            p = p + 1;
+        }
+    }
+    return n;
+}
+
+fn gt_sc_reset() -> u32 {
+    SC_N = 0;
+    return 0;
+}
+
+/// Bytes into the scratch text; the packed span (lo, hi) of what was written.
+fn gt_sc_put(s: []const u8) -> i64 {
+    const lo : u32 = SC_N;
+    var i : usize = 0;
+    while (i < s.len and (SC_N as usize) < 1024) {
+        SC[SC_N as usize] = s[i];
+        SC_N = SC_N + 1;
+        i = i + 1;
+    }
+    return jspan(lo as usize, SC_N as usize);
+}
+
+fn gt_clear() -> u32 {
+    var i : usize = 0;
+    while (i < 40) {
+        F_K[i] = W_V_NONE;
+        F_R[i] = 0;
+        i = i + 1;
+    }
+    return 0;
+}
+
+/// Slot of a name in W_NAMES (40 when it is none).
+fn gt_slot(name: []const u8) -> u32 {
+    var idx : u32 = 0;
+    var i : usize = 0;
+    while (i < W_NAMES.len) {
+        var j : usize = i;
+        while (j < W_NAMES.len and W_NAMES[j] != 32) { j = j + 1; }
+        if (name_is(W_NAMES, i, j, name)) { return idx; }
+        idx = idx + 1;
+        i = j + 1;
+    }
+    return 40;
+}
+
+fn gt_set(name: []const u8, k: u32, r: i64) -> u32 {
+    const s : u32 = gt_slot(name);
+    if (s < 40) {
+        F_K[s as usize] = k;
+        F_R[s as usize] = r;
+    }
+    return 0;
+}
+
+fn gt_val(o: []u8, n: u32, k: u32, r: i64) -> u32 {
+    if (k == W_V_STR) { return gt_jstr(o, n, gt_d(), r); }
+    if (k == W_V_RAW) { return gt_jstring(o, n, gt_d(), r); }
+    if (k == W_V_NUM) { return num(o, n, r as u64); }
+    if (k == W_V_SC) { return gt_text(o, n, SC[span_lo(r)..span_hi(r)]); }
+    return n;
+}
+
+/// fill(template, vars): {name} replaced by the value set for it, a name with none stays as written.
+fn gt_fill(o: []u8, n0: u32, t: []const u8) -> u32 {
+    var n : u32 = n0;
+    var i : usize = 0;
+    while (i < t.len) {
+        var done : bool = false;
+        if (t[i] == 123) {
+            var j : usize = i + 1;
+            while (j < t.len and is_word_byte(t[j])) { j = j + 1; }
+            if (j > i + 1 and j < t.len and t[j] == 125) {
+                const s : u32 = gt_slot(t[(i + 1)..j]);
+                if (s < 40 and F_K[s as usize] != W_V_NONE) {
+                    n = gt_val(o, n, F_K[s as usize], F_R[s as usize]);
+                    i = j + 1;
+                    done = true;
+                }
+            }
+        }
+        if (!done) {
+            n = gt_esc(o, n, t[i]);
+            i = i + 1;
+        }
+    }
+    return n;
+}
+
+// --- the rule's dates and the address ------------------------------------------------------------------------
+
+/// YYYY-MM-DD of a day number (toISOString().slice(0, 10)), written to the scratch text.
+fn gt_iso_day(day: u32) -> i64 {
+    const z : u64 = (day as u64) + 719468;
+    const era : u64 = z / 146097;
+    const doe : u64 = z % 146097;
+    const yoe : u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
+    var y : u64 = yoe + era * 400;
+    const doy : u64 = doe - (365 * yoe + yoe / 4 - yoe / 100);
+    const mp : u64 = (5 * doy + 2) / 153;
+    const dd : u64 = doy - (153 * mp + 2) / 5 + 1;
+    var m : u64 = mp + 3;
+    if (mp >= 10) { m = mp - 9; }
+    if (m <= 2) { y = y + 1; }
+    var b : [16]u8 = [0; 16];
+    b[0] = (48 + (y / 1000) % 10) as u8;
+    b[1] = (48 + (y / 100) % 10) as u8;
+    b[2] = (48 + (y / 10) % 10) as u8;
+    b[3] = (48 + y % 10) as u8;
+    b[4] = 45;
+    b[5] = (48 + m / 10) as u8;
+    b[6] = (48 + m % 10) as u8;
+    b[7] = 45;
+    b[8] = (48 + dd / 10) as u8;
+    b[9] = (48 + dd % 10) as u8;
+    return gt_sc_put(b[0..10]);
+}
+
+/// Before the epoch day there is no puzzle yet, so the clock is held at Gatle #1.
+fn gt_today() -> u32 {
+    var t : u32 = (js_floor(WALL / W_DAY_MS)) as u32;
+    if (t < K_EPOCH_DAY) { t = K_EPOCH_DAY; }
+    return t;
+}
+
+/// URLSearchParams' percent-decoding of s[lo .. hi) into dst: + is a space, %XX a byte; returns the length.
+fn gt_unescape(dst: []u8, s: []const u8, lo: usize, hi: usize) -> u32 {
+    var n : u32 = 0;
+    var i : usize = lo;
+    while (i < hi) {
+        var b : u8 = s[i];
+        if (b == 43) { b = 32; }
+        if (b == 37 and i + 2 < hi and jv_digit_value(s[i + 1]) < 16 and jv_digit_value(s[i + 2]) < 16) {
+            b = (jv_digit_value(s[i + 1]) * 16 + jv_digit_value(s[i + 2])) as u8;
+            i = i + 2;
+        }
+        if ((n as usize) < dst.len) { dst[n as usize] = b; n = n + 1; }
+        i = i + 1;
+    }
+    return n;
+}
+
+/// /^\d{1,7}$/ then Number(): the value, or -1.
+fn gt_int(s: []const u8) -> i64 {
+    if (s.len < 1 or s.len > 7) { return -1; }
+    var v : i64 = 0;
+    var i : usize = 0;
+    while (i < s.len) {
+        if (s[i] < 48 or s[i] > 57) { return -1; }
+        v = v * 10 + ((s[i] - 48) as i64);
+        i = i + 1;
+    }
+    return v;
+}
+
+/// rule.js parseHash: "#d=&g=." or "#p=&g=..."; anything unreadable is ignored.
+fn gt_parse_hash(h: []const u8) -> u32 {
+    P_DAY = -1;
+    P_PRACTICE = -1;
+    P_GN = 0;
+    var start : usize = 0;
+    if (h.len > 0 and h[0] == 35) { start = 1; }
+    var seen_d : bool = false;
+    var seen_p : bool = false;
+    var seen_g : bool = false;
+    var key : [64]u8 = [0; 64];
+    var i : usize = start;
+    while (i <= h.len) {
+        var j : usize = i;
+        while (j < h.len and h[j] != 38) { j = j + 1; }
+        // the pair is h[i .. j)
+        if (j > i) {
+            var eq : usize = i;
+            while (eq < j and h[eq] != 61) { eq = eq + 1; }
+            const kl : u32 = gt_unescape(key[0..64], h, i, eq);
+            var vs : usize = eq;
+            if (eq < j) { vs = eq + 1; }
+            if (kl == 1 and key[0] == 100 and !seen_d) {
+                seen_d = true;
+                const vl : u32 = gt_unescape(KV[0..4096], h, vs, j);
+                P_DAY = gt_int(KV[0..(vl as usize)]);
+            }
+            if (kl == 1 and key[0] == 112 and !seen_p) {
+                seen_p = true;
+                const vl : u32 = gt_unescape(KV[0..4096], h, vs, j);
+                P_PRACTICE = gt_int(KV[0..(vl as usize)]);
+            }
+            if (kl == 1 and key[0] == 103 and !seen_g) {
+                seen_g = true;
+                const vl : u32 = gt_unescape(KV[0..4096], h, vs, j);
+                var a : usize = 0;
+                while (a <= (vl as usize)) {
+                    var z : usize = a;
+                    while (z < (vl as usize) and KV[z] != 46) { z = z + 1; }
+                    const g : i64 = gt_int(KV[a..z]);
+                    if (g >= 0 and g <= (K_MAX_GUESS as i64) and P_GN < (K_GUESSES as u32)) {
+                        P_G[P_GN as usize] = g as u32;
+                        P_GN = P_GN + 1;
+                    }
+                    a = z + 1;
+                }
+            }
+        }
+        i = j + 1;
+    }
+    return 0;
+}
+
+/// rule.js formatHash: "#d=" or "#p=", then "&g=1.2.3" when there are guesses.
+fn gt_format_hash(dst: []u8) -> u32 {
+    var n : u32 = put(dst, 0, 35);
+    if (G_PRACTICE) {
+        n = raw(dst, n, "p=");
+        n = num(dst, n, G_INDEX as u64);
+    } else {
+        n = raw(dst, n, "d=");
+        n = num(dst, n, G_DAY as u64);
+    }
+    if (G_N > 0) {
+        n = raw(dst, n, "&g=");
+        var i : u32 = 0;
+        while (i < G_N) {
+            if (i > 0) { n = put(dst, n, 46); }
+            n = num(dst, n, G_GUESS[i as usize] as u64);
+            i = i + 1;
+        }
+    }
+    return n;
+}
+
+// --- the games the visit remembers -------------------------------------------------------------------------
+
+fn gt_mem_key(practice: bool, v: u32) -> u32 {
+    if (practice) { return 2147483648 + v; }
+    return v;
+}
+
+fn gt_mem_find(key: u32) -> i64 {
+    var i : u32 = 0;
+    while (i < MEM_USED) {
+        if (MEM_KEY[i as usize] == key) { return i as i64; }
+        i = i + 1;
+    }
+    return -1;
+}
+
+/// memory.get(key) || [] into G_GUESS.
+fn gt_mem_load(key: u32) -> u32 {
+    G_N = 0;
+    const at : i64 = gt_mem_find(key);
+    if (at >= 0) {
+        const base : usize = (at as usize) * 6;
+        G_N = MEM_N[at as usize];
+        var i : usize = 0;
+        while (i < (G_N as usize)) {
+            G_GUESS[i] = MEM_GUESS[base + i];
+            i = i + 1;
+        }
+    }
+    return 0;
+}
+
+fn gt_mem_save() -> u32 {
+    const key : u32 = gt_mem_key(G_PRACTICE, G_INDEX);
+    var k2 : u32 = key;
+    if (!G_PRACTICE) { k2 = G_DAY; }
+    var at : i64 = gt_mem_find(k2);
+    if (at < 0) {
+        if (MEM_USED < W_MEM_MAX) {
+            at = MEM_USED as i64;
+            MEM_USED = MEM_USED + 1;
+        } else {
+            at = MEM_NEXT as i64;
+            MEM_NEXT = (MEM_NEXT + 1) % W_MEM_MAX;
+        }
+        MEM_KEY[at as usize] = k2;
+    }
+    const base : usize = (at as usize) * 6;
+    MEM_N[at as usize] = G_N;
+    var i : usize = 0;
+    while (i < (G_N as usize)) {
+        MEM_GUESS[base + i] = G_GUESS[i];
+        i = i + 1;
+    }
+    return 0;
+}
+
+fn gt_game_from_hash() -> u32 {
+    gt_parse_hash(HASH_IN[0..(HASH_IN_N as usize)]);
+    if (P_PRACTICE >= 0 and P_PRACTICE < (POOL_N as i64)) {
+        G_PRACTICE = true;
+        G_DAY = 0;
+        G_INDEX = P_PRACTICE as u32;
+        G_N = P_GN;
+        var i : usize = 0;
+        while (i < (P_GN as usize)) {
+            G_GUESS[i] = P_G[i];
+            i = i + 1;
+        }
+        return 0;
+    }
+    const t : u32 = gt_today();
+    var day : u32 = t;
+    if (P_DAY >= (K_EPOCH_DAY as i64) and P_DAY <= (t as i64)) { day = P_DAY as u32; }
+    G_PRACTICE = false;
+    G_DAY = day;
+    G_INDEX = day_index(day as u64, K_EPOCH_DAY as u64, POOL_N as u64) as u32;
+    if (P_DAY == (day as i64) and P_GN > 0) {
+        G_N = P_GN;
+        var i : usize = 0;
+        while (i < (P_GN as usize)) {
+            G_GUESS[i] = P_G[i];
+            i = i + 1;
+        }
+    } else {
+        gt_mem_load(day);
+    }
+    return 0;
+}
+
+/// saveHash(): the game is remembered for the visit and its fragment is to be shown.
+fn gt_save() -> u32 {
+    gt_mem_save();
+    HASH_DIRTY = true;
+    return 0;
+}
+
+/// newPractice(): a random module, never the one on the page.
+fn gt_new_practice() -> u32 {
+    var i : u32 = js_floor(RAND * (POOL_N as f64)) as u32;
+    if (POOL_N > 1 and i == G_INDEX) { i = (i + 1) % POOL_N; }
+    G_PRACTICE = true;
+    G_DAY = 0;
+    G_INDEX = i;
+    gt_mem_load(gt_mem_key(true, i));
+    return 0;
+}
+
+// --- effects ---------------------------------------------------------------------------------------------
+
+fn gt_eff_clear() -> u32 {
+    EQ_N = 0;
+    return 0;
+}
+
+fn gt_eff(k: u32, a: u32) -> u32 {
+    if (EQ_N < 8) {
+        EQ_KIND[EQ_N as usize] = k;
+        EQ_ARG[EQ_N as usize] = a;
+        EQ_N = EQ_N + 1;
+    }
+    return 0;
+}
+
+// --- the pool and the sources -------------------------------------------------------------------------------
+
+fn gt_pf(sp: i64, key: []const u8) -> i64 {
+    return obj_get(gt_d(), sp, key);
+}
+
+fn gt_cur() -> i64 {
+    return POOL_SP[G_INDEX as usize];
+}
+
+/// The first puzzle whose src is the same as that of puzzle i: sources are fetched once per path.
+fn gt_src_slot(i: u32) -> u32 {
+    const d : []const u8 = gt_d();
+    const s : i64 = obj_get(d, POOL_SP[i as usize], "src");
+    var j : u32 = 0;
+    while (j < i) {
+        const t : i64 = obj_get(d, POOL_SP[j as usize], "src");
+        if (span_hi(s) - span_lo(s) == span_hi(t) - span_lo(t)) {
+            var k : usize = 0;
+            var same : bool = true;
+            while (same and k < span_hi(s) - span_lo(s)) {
+                if (d[span_lo(s) + k] != d[span_lo(t) + k]) { same = false; }
+                k = k + 1;
+            }
+            if (same) { return j; }
+        }
+        j = j + 1;
+    }
+    return i;
+}
+
+/// sourceOf(p): the file is asked for once.
+fn gt_src_need() -> u32 {
+    const slot : u32 = gt_src_slot(G_INDEX);
+    if (SRC_STATE[slot as usize] == 0) {
+        SRC_STATE[slot as usize] = 1;
+        gt_eff(1, slot);
+    }
+    return 0;
+}
+
+fn gt_src_state() -> u32 {
+    return SRC_STATE[gt_src_slot(G_INDEX) as usize];
+}
+
+fn gt_src_bytes() -> []const u8 {
+    const slot : usize = gt_src_slot(G_INDEX) as usize;
+    return SRC_STORE[(SRC_OFF[slot] as usize)..((SRC_OFF[slot] + SRC_LEN[slot]) as usize)];
+}
+
+/// The decoded string of a pool field equals bytes.
+fn gt_str_is(d: []const u8, sp: i64, b: []const u8) -> bool {
+    if (kind(d, sp) != J_STRING or str_len(d, sp) != (b.len as u32)) { return false; }
+    var k : usize = 0;
+    while (k < b.len) {
+        if (str_byte(d, sp, k as u32) != b[k]) { return false; }
+        k = k + 1;
+    }
+    return true;
+}
+
+pub fn w_data_failed(i: u32) -> u32 {
+    gt_eff_clear();
+    DATA_STATE = 2;
+    return 0;
+}
+
+pub fn w_data(i: u32, s: []const u8) -> u32 {
+    gt_eff_clear();
+    if (s.len > (W_DATA_CAP as usize)) {
+        DATA_STATE = 2;
+        return 0;
+    }
+    var k : usize = 0;
+    while (k < s.len) {
+        DATA[k] = s[k];
+        k = k + 1;
+    }
+    DATA_N = s.len as u32;
+    DATA_STATE = 2;
+    const d : []const u8 = gt_d();
+    if (!jv_valid(d)) { return 0; }
+    D_ROOT = root(d);
+    D_POOL = obj_get(d, D_ROOT, "pool");
+    if (kind(d, D_POOL) != J_ARRAY) { return 0; }
+    // the puzzles, once; a pool of none is a failure
+    var p : usize = ts_first_of(d, D_POOL);
+    POOL_N = 0;
+    ZEROFF = 0;
+    var total : u32 = 0;
+    while (p + 1 < span_hi(D_POOL)) {
+        if (POOL_N < W_POOL_MAX) {
+            const sp : i64 = jspan(p, value_end(d, p));
+            POOL_SP[POOL_N as usize] = sp;
+            POOL_N = POOL_N + 1;
+        }
+        total = total + 1;
+        const e : usize = value_end(d, p);
+        var q : usize = skip_ws(d, e);
+        if (q < span_hi(D_POOL) and d[q] == 44) { q = skip_ws(d, q + 1); }
+        p = q;
+    }
+    if (total == 0) { return 0; }
+    var z : u32 = 0;
+    var i2 : u32 = 0;
+    while (i2 < POOL_N) {
+        const ff : i64 = obj_get(d, obj_get(d, POOL_SP[i2 as usize], "answer"), "ff");
+        if (kind(d, ff) == J_NUMBER and num_f64(d, ff) == 0.0) { z = z + 1; }
+        i2 = i2 + 1;
+    }
+    ZEROFF = z;
+    D_YOSYS = obj_get(d, D_ROOT, "yosys");
+    D_CORPUS = obj_get(d, D_ROOT, "corpus");
+    D_DATE = obj_get(d, D_ROOT, "date_utc");
+    DATA_STATE = 1;
+    gt_game_from_hash();
+    gt_save();
+    gt_draw_event();
+    return 0;
+}
+
+fn ts_first_of(d: []const u8, sp: i64) -> usize {
+    return skip_ws(d, span_lo(sp) + 1);
+}
+
+/// draw(): the whole page is made again.
+fn gt_draw_event() -> u32 {
+    HDR_T = gt_today();
+    CLEAR_INPUT = true;
+    MSG_KIND = 0;
+    DET_OPEN = false;
+    SHARE_EPOCH = SHARE_EPOCH + 1;
+    COPY_MSG = 0;
+    gt_pane_start();
+    return 0;
+}
+
+/// show(): the body of the code box is made for the tab that is open.
+fn gt_pane_start() -> u32 {
+    VER_STATE = 0;
+    VER_PENDING = false;
+    gt_src_need();
+    if (V_TAB == 1) {
+        VER_PENDING = true;
+        VER_T0 = NOW;
+        if (gt_src_state() == 3) {
+            VER_PENDING = false;
+            VER_STATE = 2;
+        }
+    }
+    return 0;
+}
+
+pub fn w_fetched(id: u32, ok_: u32, s: []const u8) -> u32 {
+    gt_eff_clear();
+    if (id >= 256) { return 0; }
+    const slot : usize = id as usize;
+    if (ok_ == 0) {
+        SRC_STATE[slot] = 3;
+    } else {
+        var from : usize = 0;
+        if (s.len >= 3 and s[0] == 239 and s[1] == 187 and s[2] == 191) { from = 3; }
+        const n : usize = s.len - from;
+        if ((SRC_USED as usize) + n > (W_SRC_TOTAL as usize)) {
+            SRC_STATE[slot] = 3;
+        } else {
+            var k : usize = 0;
+            while (k < n) {
+                SRC_STORE[(SRC_USED as usize) + k] = s[from + k];
+                k = k + 1;
+            }
+            SRC_OFF[slot] = SRC_USED;
+            SRC_LEN[slot] = n as u32;
+            SRC_USED = SRC_USED + (n as u32);
+            sha256_hex(SRC_STORE[(SRC_OFF[slot] as usize)..((SRC_OFF[slot] + SRC_LEN[slot]) as usize)], SHA_H[0..8], SHA_W[0..64], SHA_O[0..64]);
+            var b : usize = 0;
+            while (b < 64) {
+                SRC_SHA[slot * 64 + b] = SHA_O[b];
+                b = b + 1;
+            }
+            SRC_STATE[slot] = 2;
+        }
+    }
+    if (VER_PENDING and DATA_STATE == 1 and gt_src_slot(G_INDEX) == id and SRC_STATE[slot] == 3) {
+        VER_PENDING = false;
+        VER_STATE = 2;
+    }
+    return 0;
+}
+
+pub fn w_compile_need(dst: []u8) -> u32 {
+    if (!VER_PENDING or DATA_STATE != 1 or gt_src_state() != 2) { return 0; }
+    const s : []const u8 = gt_src_bytes();
+    if (s.len > dst.len or s.len > (W_COMPILE_CAP as usize)) {
+        VER_PENDING = false;
+        VER_STATE = 2;
+        return 0;
+    }
+    var k : usize = 0;
+    while (k < s.len) {
+        dst[k] = s[k];
+        k = k + 1;
+    }
+    return (s.len as u32) + 1;
+}
+
+/// Length in UTF-16 units of UTF-8 text (code.length).
+fn gt_utf16_len(b: []const u8) -> u32 {
+    var n : u32 = 0;
+    var i : usize = 0;
+    while (i < b.len) {
+        if ((b[i] & 192) != 128) {
+            n = n + 1;
+            if (b[i] >= 240) { n = n + 1; }
+        }
+        i = i + 1;
+    }
+    return n;
+}
+
+pub fn w_compile_give(s: []const u8, ok_: u32, t0: f64, t1: f64) -> u32 {
+    VER_PENDING = false;
+    VER_STATE = 2;
+    if (ok_ != 0 or s.len > (W_RES_CAP as usize)) { return 0; }
+    var k : usize = 0;
+    while (k < s.len) {
+        RES[k] = s[k];
+        k = k + 1;
+    }
+    const r : []const u8 = RES[0..s.len];
+    const code : i64 = obj_get(r, obj_get(r, obj_get(r, root(r), "targets"), "verilog"), "code");
+    if (kind(r, code) != J_STRING or str_len(r, code) == 0) { return 0; }
+    // the Verilog goes through a text of UTF-16 units in tool.js; its bytes are the UTF-8 of it
+    const cn : u32 = gt_jcopy(CODE[0..1048576], r, code);
+    if (cn >= W_CODE_CAP) { return 0; }
+    CODE_N = cn;
+    sha256_hex(CODE[0..(cn as usize)], SHA_H[0..8], SHA_W[0..64], SHA_O[0..64]);
+    var b : usize = 0;
+    while (b < 64) {
+        VER_SHA[b] = SHA_O[b];
+        b = b + 1;
+    }
+    const want : i64 = gt_pf(gt_cur(), "verilog_sha256");
+    VER_SAME = gt_str_is(gt_d(), want, VER_SHA[0..64]);
+    VER_BYTES = gt_utf16_len(CODE[0..(cn as usize)]);
+    VER_MS = t1 - VER_T0;
+    VER_STATE = 1;
+    return 0;
+}
+
+// --- the page ------------------------------------------------------------------------------------------------------
+
+pub var SHT : [1024]u8 = [0; 1024];
+
+fn gt_utf8(o: []u8, n0: u32, cp: u32) -> u32 {
+    var n : u32 = n0;
+    const w : u32 = cp_utf8_len(cp);
+    var k : u32 = 0;
+    while (k < w) {
+        n = put(o, n, cp_utf8_byte(cp, k));
+        k = k + 1;
+    }
+    return n;
+}
+
+/// The base of the page with its last two directories dropped, then the path: new URL('../../' + path, base).href
+fn gt_url(o: []u8, n0: u32, rel: []const u8) -> u32 {
+    var n : u32 = n0;
+    var i : usize = 0;
+    // the origin: up to the first slash after "://"
+    var o_end : usize = 0;
+    while (i + 2 < (BASE_N as usize)) {
+        if (BASE[i] == 58 and BASE[i + 1] == 47 and BASE[i + 2] == 47) { o_end = i + 3; i = BASE_N as usize; }
+        i = i + 1;
+    }
+    while (o_end < (BASE_N as usize) and BASE[o_end] != 47) { o_end = o_end + 1; }
+    // the segments of the directory path
+    var segs : u32 = 0;
+    var q : usize = o_end;
+    while (q < (BASE_N as usize)) {
+        if (BASE[q] == 47 and q + 1 < (BASE_N as usize) and BASE[q + 1] != 47) { segs = segs + 1; }
+        q = q + 1;
+    }
+    var keep : u32 = 0;
+    if (segs > 2) { keep = segs - 2; }
+    // write the origin and the first `keep` segments
+    var k : usize = 0;
+    while (k < o_end) {
+        n = put_escaped(o, n, BASE[k]);
+        k = k + 1;
+    }
+    var seen : u32 = 0;
+    var p : usize = o_end;
+    while (p < (BASE_N as usize) and seen < keep) {
+        if (BASE[p] == 47 and p + 1 < (BASE_N as usize) and BASE[p + 1] != 47) {
+            seen = seen + 1;
+            var e : usize = p + 1;
+            while (e < (BASE_N as usize) and BASE[e] != 47) { e = e + 1; }
+            var c : usize = p;
+            while (c < e) {
+                n = put_escaped(o, n, BASE[c]);
+                c = c + 1;
+            }
+            p = e;
+        } else {
+            p = p + 1;
+        }
+    }
+    n = put(o, n, 47);
+    return gt_text(o, n, rel);
+}
+
+/// Is the line src[lo .. hi) nothing but a comment: white space, then ; or //.
+fn gt_is_comment(src: []const u8, lo: usize, hi: usize) -> bool {
+    var p : usize = lo;
+    var w : u32 = 1;
+    while (p < hi and w > 0) {
+        w = jv_ws_at(src, p as u32, hi as u32);
+        p = p + (w as usize);
+    }
+    if (p >= hi) { return false; }
+    if (src[p] == 59) { return true; }
+    return src[p] == 47 and p + 1 < hi and src[p + 1] == 47;
+}
+
+fn gt_src_pane(o: []u8, n0: u32) -> u32 {
+    const st : u32 = gt_src_state();
+    var n : u32 = n0;
+    if (st == 3) {
+        n = raw(o, n, "

"); + n = gt_text(o, n, SAY_SOURCE_FAILED); + return raw(o, n, "

"); + } + if (st != 2) { + n = raw(o, n, "

"); + n = gt_text(o, n, SAY_LOADING); + return raw(o, n, "

"); + } + const src : []const u8 = gt_src_bytes(); + var end : usize = src.len; + if (end > 0 and src[end - 1] == 10) { end = end - 1; } + // the lines: src[ls .. le) separated by line feeds + var hidden : u32 = 0; + var shown : u32 = 0; + var first_empty : bool = false; + var ls : usize = 0; + var count_done : bool = false; + while (!count_done) { + var le : usize = ls; + while (le < end and src[le] != 10) { le = le + 1; } + const com : bool = gt_is_comment(src, ls, le); + if (com) { hidden = hidden + 1; } + if (V_COMMENTS or !com) { + if (shown == 0 and le == ls) { first_empty = true; } + shown = shown + 1; + } + if (le >= end) { count_done = true; } else { ls = le + 1; } + } + n = raw(o, n, "
");
+    if (first_empty and shown >= 2) { n = put(o, n, 10); }
+    var written : u32 = 0;
+    ls = 0;
+    count_done = false;
+    while (!count_done) {
+        var le : usize = ls;
+        while (le < end and src[le] != 10) { le = le + 1; }
+        if (V_COMMENTS or !gt_is_comment(src, ls, le)) {
+            if (written > 0) { n = put(o, n, 10); }
+            written = written + 1;
+            var c : usize = ls;
+            while (c < le) {
+                n = gt_esc(o, n, src[c]);
+                c = c + 1;
+            }
+        }
+        if (le >= end) { count_done = true; } else { ls = le + 1; }
+    }
+    n = raw(o, n, "
"); + n = gt_jstr(o, n, d, gt_pf(gt_cur(), "path")); + n = raw(o, n, "
"); + // the digest of the source against the one yosys read + const want : i64 = gt_pf(gt_cur(), "spec_sha256"); + const slot : usize = gt_src_slot(G_INDEX) as usize; + if (!gt_str_is(d, want, SRC_SHA[(slot * 64)..(slot * 64 + 64)])) { + n = raw(o, n, "

"); + n = gt_text(o, n, SAY_SOURCE_DRIFT); + n = raw(o, n, "

"); + } + return n; +} + +fn gt_ver_pane(o: []u8, n0: u32) -> u32 { + var n : u32 = n0; + if (VER_STATE == 0) { + n = raw(o, n, "

"); + n = gt_text(o, n, SAY_COMPILING); + return raw(o, n, "

"); + } + if (VER_STATE == 2) { + n = raw(o, n, "

"); + n = gt_text(o, n, SAY_COMPILE_FAILED); + return raw(o, n, "

"); + } + n = raw(o, n, "
");
+    if (CODE_N > 0 and CODE[0] == 10) { n = put(o, n, 10); }
+    n = gt_text(o, n, CODE[0..(CODE_N as usize)]);
+    n = raw(o, n, "
"); + if (VER_SAME) { n = raw(o, n, "

"); } else { n = raw(o, n, "

"); } + gt_clear(); + gt_sc_reset(); + var tb : [32]u8 = [0; 32]; + const ml : u32 = js_str_num(tb[0..32], 0, js_round(VER_MS)); + gt_set("ms", W_V_SC, gt_sc_put(tb[0..(ml as usize)])); + gt_set("sha", W_V_SC, gt_sc_put(VER_SHA[0..12])); + if (VER_SAME) { + gt_set("bytes", W_V_NUM, VER_BYTES as i64); + n = gt_fill(o, n, SAY_VERILOG_SAME); + } else { + var wb : [64]u8 = [0; 64]; + const wl : u32 = gt_jcopy(wb[0..64], gt_d(), gt_pf(gt_cur(), "verilog_sha256")); + var wn : usize = wl as usize; + if (wn > 12) { wn = 12; } + gt_set("was", W_V_SC, gt_sc_put(wb[0..wn])); + n = gt_fill(o, n, SAY_VERILOG_DRIFT); + } + return raw(o, n, "

"); +} + +fn gt_code_pane(o: []u8, n0: u32) -> u32 { + const d : []const u8 = gt_d(); + const p : i64 = gt_cur(); + var n : u32 = raw(o, n0, "

"); + n = gt_jstr(o, n, d, gt_pf(p, "id")); + n = raw(o, n, " "); + gt_clear(); + gt_set("top", W_V_STR, gt_pf(p, "top")); + gt_set("code", W_V_RAW, gt_pf(p, "code_lines")); + gt_set("lines", W_V_RAW, gt_pf(p, "lines")); + n = gt_fill(o, n, SAY_MODULE_META); + n = raw(o, n, "

"); + var i : u32 = 0; + while (i < 2) { + n = raw(o, n, ""); + i = i + 1; + } + n = raw(o, n, "
"); + if (V_TAB == 0) { n = gt_src_pane(o, n); } else { n = gt_ver_pane(o, n); } + return raw(o, n, "
"); +} + +fn gt_header(o: []u8, n0: u32) -> u32 { + var n : u32 = raw(o, n0, "
"); + var i : u32 = 0; + while (i < 2) { + n = raw(o, n, ""); + i = i + 1; + } + n = raw(o, n, "
"); + if (G_PRACTICE) { + n = raw(o, n, ""); + } + n = raw(o, n, "

"); + gt_clear(); + gt_sc_reset(); + gt_set("size", W_V_NUM, POOL_N as i64); + if (G_PRACTICE) { + n = gt_fill(o, n, SAY_PRACTICE_LINE); + } else { + gt_set("n", W_V_NUM, puzzle_number(G_DAY as u64, K_EPOCH_DAY as u64) as i64); + gt_set("date", W_V_SC, gt_iso_day(G_DAY)); + if (G_DAY == HDR_T) { + gt_set("i", W_V_NUM, (G_INDEX as i64) + 1); + n = gt_fill(o, n, SAY_DAILY_LINE); + } else { + gt_set("today", W_V_NUM, puzzle_number(HDR_T as u64, K_EPOCH_DAY as u64) as i64); + n = gt_fill(o, n, SAY_PAST_LINE); + } + } + return raw(o, n, "

"); +} + +/// The cut of yosys' version text tool.js makes: version.replace(/\s*\(.*$/, ''). +fn gt_version(dst: []u8) -> u32 { + const d : []const u8 = gt_d(); + var raw_v : [256]u8 = [0; 256]; + const vl : u32 = gt_jcopy(raw_v[0..256], d, obj_get(d, D_YOSYS, "version")); + var cut : u32 = vl; + var i : u32 = 0; + var found : bool = false; + while (i < vl and !found) { + if (raw_v[i as usize] == 40) { + // no line terminator after the bracket (the dot does not match one) + var clean : bool = true; + var j : u32 = i + 1; + while (j < vl) { + const c : u8 = raw_v[j as usize]; + if (c == 10 or c == 13) { clean = false; } + if (c == 226 and j + 2 < vl and raw_v[(j + 1) as usize] == 128 and (raw_v[(j + 2) as usize] == 168 or raw_v[(j + 2) as usize] == 169)) { clean = false; } + j = j + 1; + } + if (clean) { + found = true; + cut = i; + } + } + i = i + 1; + } + if (found) { + // the white space before the bracket goes too + var back : bool = true; + while (back and cut > 0) { + back = false; + const c : u8 = raw_v[(cut - 1) as usize]; + if (jv_is_ws(c)) { cut = cut - 1; back = true; } + if (!back and cut >= 2 and jv_ws_before(raw_v[0..256], 0, cut) == 2) { cut = cut - 2; back = true; } + if (!back and cut >= 3 and jv_ws_before(raw_v[0..256], 0, cut) == 3) { cut = cut - 3; back = true; } + } + } + var k : u32 = 0; + while (k < cut and (k as usize) < dst.len) { + dst[k as usize] = raw_v[k as usize]; + k = k + 1; + } + return k; +} + +fn gt_truth() -> u64 { + const lut : i64 = gt_pf(gt_pf(gt_cur(), "answer"), "lut"); + if (kind(gt_d(), lut) != J_NUMBER) { return 0; } + const v : i64 = num_i64(gt_d(), lut); + if (v < 0) { return 0; } + return v as u64; +} + +fn gt_won() -> bool { + var i : u32 = 0; + const t : u64 = gt_truth(); + while (i < G_N) { + if (heat(G_GUESS[i as usize] as u64, t, K_CLOSE_PCT as u64, K_WARM_PCT as u64) == HEAT_CLOSE) { return true; } + i = i + 1; + } + return false; +} + +fn gt_over() -> bool { + return gt_won() or G_N >= (K_GUESSES as u32); +} + +fn gt_board(o: []u8, n0: u32) -> u32 { + var n : u32 = raw(o, n0, "
    "); + const t : u64 = gt_truth(); + var i : u32 = 0; + while (i < (K_GUESSES as u32)) { + if (i >= G_N) { + n = raw(o, n, "
  1. "); + } else { + const g : u64 = G_GUESS[i as usize] as u64; + const h : u8 = heat(g, t, K_CLOSE_PCT as u64, K_WARM_PCT as u64); + const dr : u8 = dir(g, t, K_CLOSE_PCT as u64, K_WARM_PCT as u64); + n = raw(o, n, "
  2. "); + n = num(o, n, g); + n = raw(o, n, ""); + const ac : u32 = arrow_code(dr); + if (ac != 0) { n = gt_utf8(o, n, ac); } + n = raw(o, n, ""); + n = gt_text(o, n, SAY_DIRS[dr as usize]); + n = raw(o, n, ", "); + gt_clear(); + gt_set("close", W_V_NUM, K_CLOSE_PCT as i64); + gt_set("warm", W_V_NUM, K_WARM_PCT as i64); + n = gt_fill(o, n, SAY_HEAT[h as usize]); + n = raw(o, n, "
  3. "); + } + i = i + 1; + } + return raw(o, n, "
"); +} + +/// The share text (plain, not escaped) into SHT; its length. +fn gt_share_text() -> u32 { + RAW_OUT = true; + var n : u32 = 0; + const won : bool = gt_won(); + gt_clear(); + gt_sc_reset(); + var sb : [24]u8 = [0; 24]; + var sl : u32 = 0; + if (won) { sl = num(sb[0..24], 0, G_N as u64); } else { sl = raw(sb[0..24], 0, SAY_SHARE_LOST); } + gt_set("score", W_V_SC, gt_sc_put(sb[0..(sl as usize)])); + gt_set("max", W_V_NUM, K_GUESSES as i64); + if (G_PRACTICE) { + n = gt_fill(SHT[0..1024], n, SAY_SHARE_PRACTICE); + } else { + gt_set("n", W_V_NUM, puzzle_number(G_DAY as u64, K_EPOCH_DAY as u64) as i64); + n = gt_fill(SHT[0..1024], n, SAY_SHARE_HEAD); + } + const t : u64 = gt_truth(); + var i : u32 = 0; + while (i < G_N) { + const g : u64 = G_GUESS[i as usize] as u64; + const h : u8 = heat(g, t, K_CLOSE_PCT as u64, K_WARM_PCT as u64); + const dr : u8 = dir(g, t, K_CLOSE_PCT as u64, K_WARM_PCT as u64); + n = put(SHT[0..1024], n, 10); + n = gt_utf8(SHT[0..1024], n, heat_code(h)); + if (dr != DIR_HIT) { + n = gt_utf8(SHT[0..1024], n, arrow_code(dr)); + n = gt_utf8(SHT[0..1024], n, K_EMOJI_SELECTOR as u32); + } + i = i + 1; + } + n = put(SHT[0..1024], n, 10); + n = raw(SHT[0..1024], n, K_SHARE_URL); + RAW_OUT = false; + return n; +} + +fn gt_reveal(o: []u8, n0: u32) -> u32 { + const d : []const u8 = gt_d(); + const p : i64 = gt_cur(); + const a : i64 = gt_pf(p, "answer"); + const won : bool = gt_won(); + var n : u32 = raw(o, n0, "
"); + gt_clear(); + if (won) { + n = raw(o, n, "

"); + gt_set("n", W_V_NUM, G_N as i64); + gt_set("max", W_V_NUM, K_GUESSES as i64); + gt_set("close", W_V_NUM, K_CLOSE_PCT as i64); + n = gt_fill(o, n, SAY_WON); + } else { + n = raw(o, n, "

"); + n = gt_text(o, n, SAY_LOST); + } + n = raw(o, n, "

"); + gt_clear(); + gt_set("lut", W_V_RAW, gt_pf(a, "lut")); + n = gt_fill(o, n, SAY_ANSWER); + n = raw(o, n, "

"); + gt_clear(); + gt_set("ff", W_V_RAW, gt_pf(a, "ff")); + gt_set("carry4", W_V_RAW, gt_pf(a, "carry4")); + gt_set("muxf7", W_V_RAW, gt_pf(a, "muxf7")); + gt_set("muxf8", W_V_RAW, gt_pf(a, "muxf8")); + gt_set("dsp", W_V_RAW, gt_pf(a, "dsp")); + gt_set("cells", W_V_RAW, gt_pf(a, "cells")); + n = gt_fill(o, n, SAY_REVEAL); + n = raw(o, n, "

"); + const oo : i64 = gt_pf(p, "other"); + const ol : i64 = gt_pf(oo, "lut"); + const olk : u8 = kind(d, ol); + if (olk != J_NONE and olk != J_NULL) { + n = raw(o, n, "

"); + gt_clear(); + gt_sc_reset(); + gt_set("mode", W_V_SC, gt_sc_put(K_OTHER_SYNTH)); + const al : i64 = gt_pf(a, "lut"); + const od : i64 = gt_pf(oo, "dsp"); + const ad : i64 = gt_pf(a, "dsp"); + if (gt_same_value(d, ol, al) and gt_same_value(d, od, ad)) { + n = gt_fill(o, n, SAY_OTHER_SAME); + } else { + gt_set("lut", W_V_RAW, ol); + gt_set("dsp", W_V_RAW, od); + n = gt_fill(o, n, SAY_OTHER_DIFF); + } + n = raw(o, n, "

"); + } + n = raw(o, n, "
"); + n = gt_text(o, n, SAY_BY_TYPE); + n = raw(o, n, "

"); + n = gt_types(o, n, d, gt_pf(a, "by_type")); + n = raw(o, n, "

"); + // the share box + const sl : u32 = gt_share_text(); + n = raw(o, n, "

"); + n = gt_text(o, n, SAY_SHARE_LABEL); + n = raw(o, n, "

");
+    n = gt_text(o, n, SHT[0..(sl as usize)]);
+    n = raw(o, n, "
"); + if (COPY_MSG == 1) { n = gt_text(o, n, SAY_COPIED); } + if (COPY_MSG == 2) { n = gt_text(o, n, SAY_COPY_FAILED); } + return raw(o, n, "
"); +} + +/// a === b for two JSON values that are numbers or words. +fn gt_same_value(d: []const u8, a: i64, b: i64) -> bool { + const ka : u8 = kind(d, a); + if (ka != kind(d, b)) { return false; } + if (ka == J_NUMBER) { return num_f64(d, a) == num_f64(d, b); } + if (ka == J_STRING) { + if (str_len(d, a) != str_len(d, b)) { return false; } + var k : u32 = 0; + while (k < str_len(d, a)) { + if (str_byte(d, a, k) != str_byte(d, b, k)) { return false; } + k = k + 1; + } + return true; + } + return true; +} + +/// Is key a before key b, as JavaScript's < compares two strings (UTF-16 order, here the UTF-8 order)? +fn gt_key_less(d: []const u8, a: i64, b: i64) -> bool { + const la : u32 = str_len(d, a); + const lb : u32 = str_len(d, b); + var k : u32 = 0; + while (k < la and k < lb) { + const x : u8 = str_byte(d, a, k); + const y : u8 = str_byte(d, b, k); + if (x != y) { return x < y; } + k = k + 1; + } + return la < lb; +} + +/// Object.entries(by_type).sort(by key).map(([k, n]) => `${k} ${n}`).join(', ') +fn gt_types(o: []u8, n0: u32, d: []const u8, sp: i64) -> u32 { + var n : u32 = n0; + var cnt : u32 = 0; + if (kind(d, sp) == J_OBJECT) { + const hi : usize = span_hi(sp); + var p : usize = skip_ws(d, span_lo(sp) + 1); + while (p < hi and d[p] == 34 and cnt < 64) { + const ke : usize = string_end(d, p); + if (ke > hi) { break; } + const q : usize = skip_ws(d, ke); + if (q >= hi or d[q] != 58) { break; } + const v : usize = skip_ws(d, q + 1); + const ve : usize = value_end(d, v); + if (ve > hi) { break; } + // insertion sort by key + var at : u32 = cnt; + const ksp : i64 = jspan(p, ke); + while (at > 0 and gt_key_less(d, ksp, BT_K[(at - 1) as usize])) { + BT_K[at as usize] = BT_K[(at - 1) as usize]; + BT_V[at as usize] = BT_V[(at - 1) as usize]; + at = at - 1; + } + BT_K[at as usize] = ksp; + BT_V[at as usize] = jspan(v, ve); + cnt = cnt + 1; + p = skip_ws(d, ve); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + } + var i : u32 = 0; + while (i < cnt) { + if (i > 0) { n = raw(o, n, ", "); } + n = gt_jstr(o, n, d, BT_K[i as usize]); + n = put(o, n, 32); + n = gt_jstring(o, n, d, BT_V[i as usize]); + i = i + 1; + } + return n; +} + +fn gt_form(o: []u8, n0: u32) -> u32 { + var n : u32 = raw(o, n0, "

"); + gt_clear(); + gt_set("n", W_V_NUM, (K_GUESSES as i64) - (G_N as i64)); + gt_set("max", W_V_NUM, K_GUESSES as i64); + n = gt_fill(o, n, SAY_GUESSES_LEFT); + n = raw(o, n, "

"); + if (MSG_KIND == 1) { + gt_clear(); + gt_set("max", W_V_NUM, K_MAX_GUESS as i64); + n = gt_fill(o, n, SAY_BAD_GUESS); + } + if (MSG_KIND == 2) { n = gt_text(o, n, SAY_ALREADY); } + return raw(o, n, "

"); +} + +fn gt_play(o: []u8, n0: u32) -> u32 { + const d : []const u8 = gt_d(); + var n : u32 = raw(o, n0, "

"); + n = gt_text(o, n, SAY_QUESTION); + n = raw(o, n, "

"); + gt_clear(); + gt_sc_reset(); + var vb : [256]u8 = [0; 256]; + const vl : u32 = gt_version(vb[0..256]); + gt_set("synth", W_V_STR, obj_get(d, D_YOSYS, "answer")); + gt_set("version", W_V_SC, gt_sc_put(vb[0..(vl as usize)])); + gt_set("close", W_V_NUM, K_CLOSE_PCT as i64); + gt_set("warm", W_V_NUM, K_WARM_PCT as i64); + n = gt_fill(o, n, SAY_RULE_NOTE); + n = raw(o, n, "

"); + n = gt_board(o, n); + if (gt_over()) { n = gt_reveal(o, n); } else { n = gt_form(o, n); } + return raw(o, n, "
"); +} + +pub fn w_render(dst: []u8) -> u32 { + var n : u32 = 0; + if (DATA_STATE == 0) { + n = raw(dst, 0, "

"); + n = gt_text(dst, n, SAY_LOADING); + n = raw(dst, n, "

"); + } else { + if (DATA_STATE == 2) { + n = raw(dst, 0, "

"); + n = gt_text(dst, n, SAY_LOAD_FAILED); + n = raw(dst, n, "

"); + } else { + const d : []const u8 = gt_d(); + n = gt_header(dst, 0); + n = raw(dst, n, "
"); + n = gt_code_pane(dst, n); + n = gt_play(dst, n); + n = raw(dst, n, "

"); + n = gt_text(dst, n, SAY_NO_STREAKS); + n = raw(dst, n, "

"); + gt_clear(); + gt_set("size", W_V_NUM, POOL_N as i64); + gt_set("specs", W_V_RAW, obj_get(d, D_CORPUS, "specs")); + gt_set("candidates", W_V_RAW, obj_get(d, D_CORPUS, "candidates")); + gt_set("date", W_V_STR, D_DATE); + gt_set("zeroff", W_V_NUM, ZEROFF as i64); + gt_set("min", W_V_NUM, K_MIN_LUTS as i64); + gt_set("max", W_V_NUM, K_MAX_LUTS as i64); + gt_set("lines", W_V_NUM, K_MAX_CODE_LINES as i64); + gt_set("cols", W_V_NUM, K_MAX_CODE_COLS as i64); + n = gt_fill(dst, n, SAY_POOL_NOTE); + n = raw(dst, n, "

"); + } + } + if (!ok(n)) { return 0; } + return n; +} + +// --- events ----------------------------------------------------------------------------------------------------- + +pub fn w_base(u: []const u8) -> u32 { + var k : usize = 0; + while (k < u.len and k < 1024) { + BASE[k] = u[k]; + k = k + 1; + } + BASE_N = k as u32; + return 0; +} + +pub fn w_wall(ms: f64) -> u32 { + WALL = ms; + return 0; +} + +pub fn w_now(t: f64) -> u32 { + NOW = t; + return 0; +} + +pub fn w_rand(r: f64) -> u32 { + RAND = r; + return 0; +} + +fn gt_store_hash(h: []const u8) -> u32 { + var k : usize = 0; + while (k < h.len and k < 4096) { + HASH_IN[k] = h[k]; + k = k + 1; + } + HASH_IN_N = k as u32; + return 0; +} + +pub fn w_init(h: []const u8) -> u32 { + gt_eff_clear(); + DATA_STATE = 0; + gt_store_hash(h); + return 0; +} + +pub fn w_hash(h: []const u8) -> u32 { + gt_eff_clear(); + gt_store_hash(h); + if (DATA_STATE == 1) { + gt_game_from_hash(); + gt_draw_event(); + } + return 0; +} + +pub fn w_hash_out(dst: []u8) -> u32 { + if (!HASH_DIRTY or DATA_STATE != 1) { return 0; } + HASH_DIRTY = false; + return gt_format_hash(dst); +} + +pub fn w_input(i: u32, s: []const u8) -> u32 { + gt_eff_clear(); + var k : usize = 0; + while (k < s.len and k < 255) { + INPUT_TEXT[k] = s[k]; + k = k + 1; + } + INPUT_N = k as u32; + return 0; +} + +pub fn w_value(i: u32, dst: []u8) -> u32 { + if (i == 0 and CLEAR_INPUT) { + CLEAR_INPUT = false; + INPUT_N = 0; + return 0; + } + return 4294967295; +} + +pub fn w_toggle(i: u32, open: u32) -> u32 { + DET_OPEN = open != 0; + return 0; +} + +/// What the browser lets through before the page's own check: the box is required, holds a number, which is +/// at least 0, at most K_MAX_GUESS and whole (step 1). +fn gt_box_valid(s: []const u8) -> bool { + if (s.len == 0) { return false; } + var i : usize = 0; + if (s[i] == 45) { i = i + 1; } + var nd : usize = 0; + while (i < s.len and s[i] >= 48 and s[i] <= 57) { + i = i + 1; + nd = nd + 1; + } + var fd : usize = 0; + if (i < s.len and s[i] == 46) { + i = i + 1; + while (i < s.len and s[i] >= 48 and s[i] <= 57) { + i = i + 1; + fd = fd + 1; + } + if (fd == 0) { return false; } + } else { + if (nd == 0) { return false; } + } + if (i < s.len and (s[i] == 101 or s[i] == 69)) { + i = i + 1; + if (i < s.len and (s[i] == 43 or s[i] == 45)) { i = i + 1; } + var ed : usize = 0; + while (i < s.len and s[i] >= 48 and s[i] <= 57) { + i = i + 1; + ed = ed + 1; + } + if (ed == 0) { return false; } + } + if (i != s.len) { return false; } + const v : f64 = js_parse_num(s, 0, s.len); + if (v < 0.0 or v > (K_MAX_GUESS as f64)) { return false; } + return v == js_floor(v); +} + +/// The form's submit handler, after the browser has let the value through. +fn gt_submit() -> u32 { + const s : []const u8 = INPUT_TEXT[0..(INPUT_N as usize)]; + if (!gt_box_valid(s)) { return 0; } + var digits : bool = true; + var v : u64 = 0; + var i : usize = 0; + while (i < s.len) { + if (s[i] < 48 or s[i] > 57) { digits = false; } + if (v < 100000000) { v = v * 10 + ((s[i] - 48) as u64); } + i = i + 1; + } + if (!digits or v > (K_MAX_GUESS as u64)) { + MSG_KIND = 1; + return 0; + } + var k : u32 = 0; + while (k < G_N) { + if ((G_GUESS[k as usize] as u64) == v) { + MSG_KIND = 2; + return 0; + } + k = k + 1; + } + if (G_N >= (K_GUESSES as u32)) { return 0; } + G_GUESS[G_N as usize] = v as u32; + G_N = G_N + 1; + gt_save(); + // redrawPlay(): the play side is made again, the code box is left as it is + MSG_KIND = 0; + CLEAR_INPUT = true; + DET_OPEN = false; + SHARE_EPOCH = SHARE_EPOCH + 1; + COPY_MSG = 0; + if (gt_over()) { gt_eff(3, 0); } else { gt_eff(2, 0); } + return 0; +} + +pub fn w_click(i: u32) -> u32 { + gt_eff_clear(); + if (DATA_STATE != 1) { return 0; } + var nh : u32 = 2; + if (G_PRACTICE) { nh = 3; } + if (i == 0) { + G_PRACTICE = false; + G_DAY = HDR_T; + G_INDEX = day_index(HDR_T as u64, K_EPOCH_DAY as u64, POOL_N as u64) as u32; + gt_mem_load(HDR_T); + gt_save(); + gt_draw_event(); + return 0; + } + if (i == 1 or (G_PRACTICE and i == 2)) { + gt_new_practice(); + gt_save(); + gt_draw_event(); + return 0; + } + if (i == nh) { + V_TAB = 0; + gt_pane_start(); + return 0; + } + if (i == nh + 1) { + V_TAB = 1; + gt_pane_start(); + return 0; + } + var at : u32 = nh + 2; + if (V_TAB == 0 and gt_src_state() == 2) { + if (i == at) { + V_COMMENTS = !V_COMMENTS; + return 0; + } + at = at + 1; + } + if (i == at) { + if (gt_over()) { + COPY_EPOCH = SHARE_EPOCH; + gt_share_text(); + gt_eff(4, 0); + } else { + gt_submit(); + } + } + return 0; +} + +pub fn w_copied(g: u32) -> u32 { + gt_eff_clear(); + if (COPY_EPOCH == SHARE_EPOCH) { + if (g != 0) { COPY_MSG = 1; } else { COPY_MSG = 2; } + gt_eff(5, SHARE_EPOCH % 256); + } + return 0; +} + +pub fn w_timer(id: u32) -> u32 { + gt_eff_clear(); + if (id == SHARE_EPOCH % 256) { COPY_MSG = 0; } + return 0; +} + +pub fn w_effect(k: u32, dst: []u8) -> u32 { + if (k >= EQ_N) { return 0; } + const kd : u32 = EQ_KIND[k as usize]; + const a : u32 = EQ_ARG[k as usize]; + if (kd == 1) { + dst[0] = 102; + dst[1] = a as u8; + var n : u32 = raw(dst, 2, "../../"); + n = n + gt_jcopy(dst[(n as usize)..], gt_d(), gt_pf(POOL_SP[a as usize], "src")); + return n; + } + if (kd == 2) { return raw(dst, put(dst, 0, 70), "#gt-in"); } + if (kd == 3) { return raw(dst, put(dst, 0, 110), ".gt-reveal"); } + if (kd == 4) { + var n : u32 = raw(dst, put(dst, 0, 119), "pre.gt-share"); + n = put(dst, n, 10); + const sl : u32 = gt_share_text(); + var j : usize = 0; + while (j < (sl as usize)) { + n = put(dst, n, SHT[j]); + j = j + 1; + } + return n; + } + // the timer that clears the copy message + dst[0] = 116; + dst[1] = a as u8; + return dec_to(dst[2..], W_COPY_MS as u64) + 2; +} + +/// The splicer follows only the names a module itself mentions, not the names its imported functions +/// mention. This anchor mentions every helper the writer, the JSON reader, the number rules, the +/// digest and the rules of the game call. Never called. +pub fn imports_anchor(o: []u8, s: []const u8, m: []u32) -> u32 { + var n : u32 = 0; + n = n + (ok(0) as u32); + n = n + OVER; + n = n + put(o, 0, 0); + n = n + raw(o, 0, s); + n = n + put_escaped(o, 0, 0); + n = n + text(o, 0, s); + n = n + num(o, 0, 0); + n = n + attr(o, 0, s, s); + n = n + fill(o, 0, s, s, s, s, 0); + n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s); + n = n + dec_to(o, 0); + n = n + (name_is(s, 0, 0, s) as u32); + n = n + (is_word_byte(0) as u32); + n = n + (H_LT as u32); + n = n + (H_GT as u32); + n = n + (H_AMP as u32); + n = n + (H_QUOTE as u32); + n = n + (H_APOS as u32); + n = n + (H_SPACE as u32); + n = n + (H_SLASH as u32); + n = n + (H_EQ as u32); + n = n + num_grouped(o, 0, 0); + n = n + ((NO_SPAN < 0) as u32); + n = n + ((SPAN > 0) as u32); + n = n + (J_NONE as u32); + n = n + (J_OBJECT as u32); + n = n + (J_ARRAY as u32); + n = n + (J_STRING as u32); + n = n + (J_NUMBER as u32); + n = n + (J_TRUE as u32); + n = n + (J_FALSE as u32); + n = n + (J_NULL as u32); + n = n + (jspan(0, 0) as u32); + n = n + (span_lo(0) as u32); + n = n + (span_hi(0) as u32); + n = n + (skip_ws(s, 0) as u32); + n = n + (string_end(s, 0) as u32); + n = n + (value_end(s, 0) as u32); + n = n + (root(s) as u32); + n = n + (kind(s, 0) as u32); + n = n + (key_is(s, 0, 0, s) as u32); + n = n + (obj_get(s, 0, s) as u32); + n = n + arr_len(s, 0); + n = n + (arr_at(s, 0, 0) as u32); + n = n + hex4(s, 0); + n = n + escape_at(s, 0, 0); + n = n + cp_utf8_len(0); + n = n + (cp_utf8_byte(0, 0) as u32); + n = n + str_len(s, 0); + n = n + (str_byte(s, 0, 0) as u32); + n = n + JN_OVER; + n = n + JN_CAP; + n = n + ((JN_TWO52 > 0.0) as u32); + n = n + ((JN_TWO53 > 0.0) as u32); + n = n + jn_put(o, 0, 0); + n = n + jn_raw(o, 0, s); + n = n + jn_digits(o, 0); + n = n + jn_mul(o, 0, 0); + n = n + jn_mul2(o, 0, 0); + n = n + jn_mul5(o, 0, 0); + n = n + jn_shift(o, 0, 0); + n = n + (jn_cmp(s, s, 0) as u32); + n = n + (jn_expo(0.0) as u32); + n = n + (jn_mant(0.0) as u32); + n = n + ((jn_make(0, 0) > 0.0) as u32); + n = n + jn_exact(o, 0, 0); + n = n + jn_fscale(0); + n = n + (js_is_nan(0.0) as u32); + n = n + ((js_nan(0.0) > 0.0) as u32); + n = n + ((js_inf(1.0) > 0.0) as u32); + n = n + ((js_floor(0.0) > 0.0) as u32); + n = n + ((js_ceil(0.0) > 0.0) as u32); + n = n + ((js_max(0.0, 0.0) > 0.0) as u32); + n = n + ((js_min(0.0, 0.0) > 0.0) as u32); + n = n + ((js_round(0.0) > 0.0) as u32); + n = n + (jn_shortest(o, 1.0) as u32); + n = n + jn_copy(o, s, 0); + n = n + jn_put_run(o, 0, s, 0, 0); + n = n + jn_layout(o, 0, s, 0, 0); + n = n + js_str_num(o, 0, 0.0); + n = n + js_to_fixed(o, 0, 0.0, 0); + n = n + js_locale_num(o, 0, 0.0); + n = n + (jn_num_ok(s, 0, 0) as u32); + n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32); + n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32); + n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32); + n = n + (jn_abs(0) as u32); + n = n + js_str_u64(o, 0, 0); + n = n + (is_true(s, 0) as u32) + (is_false(s, 0) as u32) + (is_null(s, 0) as u32); + n = n + ((num_f64(s, 0) > 0.0) as u32) + (num_i64(s, 0) as u32); + n = n + (jv_valid(s) as u32) + (jv_string_ok(s, 0) as u32) + (jv_number_ok(s, 0) as u32) + (jv_lit_ok(s, 0, s) as u32); + n = n + (jv_is_ws(0) as u32) + jv_ws_at(s, 0, 0) + jv_ws_before(s, 0, 0); + n = n + (SH_K[0] as u32) + ((SH_M32 > 0) as u32) + (sh_rotr(0, 0) as u32) + (sh_hexd(0) as u32); + n = n + sh_block(m, m, s, 0) + sha256_hex(s, m, m, o); + n = n + (DAY_MS as u32) + (HEAT_CLOSE as u32) + (HEAT_WARM as u32) + (HEAT_COLD as u32) + (DIR_HIT as u32) + (DIR_UP as u32) + (DIR_DOWN as u32); + n = n + EMOJI_HEAT[0] + ARROWS[0]; + n = n + (day_number(0) as u32) + (day_index(0, 0, 1) as u32) + (puzzle_number(0, 0) as u32) + (off100(0, 0) as u32); + n = n + (heat(0, 0, 0, 0) as u32) + (dir(0, 0, 0, 0) as u32) + heat_code(0) + arrow_code(0); + return n; +} + + +; --- Tests of the native page ------------------------------------------------------------------------------------------ +; A pool of two puzzles stands for pool.json; the source and the compiler's answer are given by hand. + +pub var G_OUT : [1048576]u8 = [0; 1048576]; +pub var G_NEED : [4096]u8 = [0; 4096]; +pub var G_EFF : [512]u8 = [0; 512]; +pub var G_HASH : [256]u8 = [0; 256]; +pub const W_G_POOL : str = "{\"date_utc\":\"2026-10-04\",\"corpus\":{\"specs\":9,\"candidates\":4},\"yosys\":{\"answer\":\"synth_xilinx -nodsp\",\"version\":\"Yosys 0.67+post (git sha1 abc)\"},\"pool\":[{\"id\":\"a/b\",\"path\":\"specs/a/b.t27\",\"src\":\"t27/files/specs/a/b.t27\",\"top\":\"B\",\"spec_sha256\":\"10b4939015dc40258ee23d285d8dff3a8b25b6d3a57b4ca8bb87ff5c25686caa\",\"verilog_sha256\":\"77616cd592ced36d8e32442146b232415fd3d53d0ec883019e336865e61052ad\",\"lines\":5,\"code_lines\":3,\"answer\":{\"lut\":200,\"ff\":0,\"carry4\":1,\"muxf7\":2,\"muxf8\":3,\"dsp\":0,\"cells\":9,\"by_type\":{\"LUT2\":5,\"CARRY4\":1,\"IBUF\":2}},\"other\":{\"mode\":\"dsp\",\"lut\":200,\"dsp\":0}},{\"id\":\"c/d\",\"path\":\"specs/c/d.t27\",\"src\":\"t27/files/specs/c/d.t27\",\"top\":\"D\",\"spec_sha256\":\"0\",\"verilog_sha256\":\"0\",\"lines\":2,\"code_lines\":2,\"answer\":{\"lut\":50,\"ff\":1,\"carry4\":0,\"muxf7\":0,\"muxf8\":0,\"dsp\":2,\"cells\":3,\"by_type\":{}},\"other\":{\"mode\":\"dsp\",\"lut\":40,\"dsp\":1}}]}"; +pub const W_G_POOL_PAST : str = "{\"date_utc\":\"2026-10-04\",\"corpus\":{\"specs\":9,\"candidates\":4},\"yosys\":{\"answer\":\"synth_xilinx -nodsp\",\"version\":\"Yosys 0.67+post (git sha1 abc)\"},\"pool\":[{\"id\":\"a/b\",\"path\":\"specs/a/b.t27\",\"src\":\"t27/files/specs/a/b.t27\",\"top\":\"B\",\"spec_sha256\":\"10b4939015dc40258ee23d285d8dff3a8b25b6d3a57b4ca8bb87ff5c25686caa\",\"verilog_sha256\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"lines\":5,\"code_lines\":3,\"answer\":{\"lut\":200,\"ff\":0,\"carry4\":1,\"muxf7\":2,\"muxf8\":3,\"dsp\":0,\"cells\":9,\"by_type\":{\"LUT2\":5,\"CARRY4\":1,\"IBUF\":2}},\"other\":{\"mode\":\"dsp\",\"lut\":200,\"dsp\":0}},{\"id\":\"c/d\",\"path\":\"specs/c/d.t27\",\"src\":\"t27/files/specs/c/d.t27\",\"top\":\"D\",\"spec_sha256\":\"0\",\"verilog_sha256\":\"0\",\"lines\":2,\"code_lines\":2,\"answer\":{\"lut\":50,\"ff\":1,\"carry4\":0,\"muxf7\":0,\"muxf8\":0,\"dsp\":2,\"cells\":3,\"by_type\":{}},\"other\":{\"mode\":\"dsp\",\"lut\":40,\"dsp\":1}}]}"; +pub const W_G_SRC : str = "; head\nmodule B;\n\n// note\nfn f() -> u8 { return 1; }\n"; +pub const W_G_ANSWER : str = "{\"targets\":{\"verilog\":{\"bytes\":20,\"code\":\"module B;\\nendmodule\\n\",\"ok\":true}}}"; +pub const W_G_DAY1 : f64 = 1791090000000.0; + +/// Does the page hold the text? (Scans for the first byte, then compares the rest.) +fn g_has(hay: []const u8, nd: []const u8) -> bool { + var at : usize = 0; + var found : bool = nd.len == 0; + while (!found and at + nd.len <= hay.len) { + if (hay[at] == nd[0]) { + var j : usize = 1; + while (j < nd.len and hay[at + j] == nd[j]) { j = j + 1; } + found = j == nd.len; + } + at = at + 1; + } + return found; +} + +fn g_page(nd: []const u8) -> bool { + return g_has(G_OUT[0..(w_render(G_OUT[0..1048576]) as usize)], nd); +} + +/// The page has pre bytes, then two bytes, then post. +fn g_page_cp(pre: []const u8, b1: u8, b2: u8, b3: u8, post: []const u8) -> bool { + var e : [200]u8 = [0; 200]; + var k : u32 = raw(e[0..200], 0, pre); + k = put(e[0..200], k, b1); + k = put(e[0..200], k, b2); + k = put(e[0..200], k, b3); + k = raw(e[0..200], k, post); + return g_page(e[0..(k as usize)]); +} + +fn g_start(pool: []const u8, wall: f64) -> u32 { + w_base("http://localhost/widgets/x/"); + w_wall(wall); + w_now(0.0); + w_init(""); + w_data(0, pool); + return 0; +} + +fn g_open() -> u32 { + g_start(W_G_POOL, W_G_DAY1); + w_fetched(0, 1, W_G_SRC); + return 0; +} + +test the_page_waits_for_the_pool_and_says_when_it_cannot_be_read { + w_init(""); + assert g_page("

Loading...

"); + w_data_failed(0); + assert g_page("

The puzzle pool could not be read.

"); + w_init(""); + w_data(0, "no"); + assert g_page("could not be read"); + w_init(""); + w_data(0, "{\"pool\":[]}"); + assert g_page("could not be read"); + w_init(""); + w_data(0, "{\"pool\":[1,}"); + assert g_page("could not be read"); + w_init(""); + w_data(0, "[1]"); + assert g_page("could not be read"); + assert w_hash_out(G_HASH[0..256]) == 0; +} + +test the_day_is_the_game_and_its_address_is_saved { + g_start(W_G_POOL, W_G_DAY1); + assert g_page("Gatle #1, 2026-10-04 UTC: puzzle 1 of 2. Every reader gets this module today."); + assert g_page("a/b top module B; 3 code lines, 5 with comments"); + assert g_page("aria-pressed=\"true\">Today"); + assert w_hash_out(G_HASH[0..256]) == 8; + assert same_bytes(G_HASH[0..8], 8, "#d=20730"); + assert w_hash_out(G_HASH[0..256]) == 0; + assert g_page("

Loading...

"); + assert w_effect(0, G_EFF[0..512]) == 2 + 6 + 23; + assert G_EFF[0] == 102; + assert G_EFF[1] == 0; + assert same_bytes(G_EFF[2..31], 29, "../../t27/files/specs/a/b.t27"); + assert w_effect(1, G_EFF[0..512]) == 0; + assert !g_page("37 puzzles"); + assert g_page("2 puzzles: the modules of 9 vendored specs"); +} + +test the_source_shows_without_its_comment_lines_and_each_comes_back_on_a_click { + g_open(); + assert g_page("
module B;\n\nfn f() -> u8 { return 1; }
"); + assert g_page("aria-pressed=\"false\">Show 2 comment linesspecs/a/b.t27"); + assert !g_page("not the one yosys read"); + w_click(4); + assert g_page("
; head\nmodule B;\n\n// note\nfn f() -> u8 { return 1; }
"); + assert g_page("aria-pressed=\"true\">Hide comment lines"); + w_click(4); + assert g_page("Show 2 comment lines"); +} + +test a_source_that_is_not_the_one_yosys_read_says_so_and_a_missing_one_too { + g_start(W_G_POOL, W_G_DAY1); + w_fetched(0, 1, "module B;\n"); + assert g_page("

This source is not the one yosys read: its sha256 differs from pool.json.

"); + g_start(W_G_POOL, W_G_DAY1); + w_fetched(0, 0, ""); + assert g_page("

The module's source could not be read.

"); + assert w_compile_need(G_NEED[0..4096]) == 0; +} + +test the_verilog_is_compiled_by_the_service_and_its_digest_is_compared { + g_open(); + w_now(100.0); + w_click(3); + assert g_page("Compiling in your browser with t27_compiler.wasm..."); + assert w_compile_need(G_NEED[0..4096]) == (W_G_SRC.len as u32) + 1; + assert w_compile_give(W_G_ANSWER, 0, 101.0, 103.4) == 0; + assert w_compile_need(G_NEED[0..4096]) == 0; + assert g_page("
module B;\nendmodule\n
"); + assert g_page("

Compiled here in 3 ms: 20 bytes, sha256 "); + assert g_page("..., the same Verilog yosys read.

"); + assert g_page("aria-pressed=\"true\">Verilog"); + // the other tab and back: the compiler is asked again + w_click(2); + assert w_compile_need(G_NEED[0..4096]) == 0; + w_click(3); + assert w_compile_need(G_NEED[0..4096]) == (W_G_SRC.len as u32) + 1; +} + +test a_different_digest_a_failed_compile_and_a_failed_source_are_named { + g_start(W_G_POOL_PAST, W_G_DAY1); + w_fetched(0, 1, W_G_SRC); + w_click(3); + w_compile_give(W_G_ANSWER, 0, 1.0, 2.0); + assert g_page("

Compiled here in 2 ms: sha256 "); + assert g_page("... is not ffffffffffff..., the Verilog yosys read.

"); + w_click(3); + w_compile_give("{\"targets\":{}}", 0, 1.0, 2.0); + assert g_page("

The compiler could not run in this browser.

"); + w_click(3); + w_compile_give("trap", 1, 0.0, 0.0); + assert g_page("The compiler could not run in this browser."); + g_start(W_G_POOL, W_G_DAY1); + w_click(3); + assert g_page("Compiling in your browser"); + w_fetched(0, 0, ""); + assert w_compile_need(G_NEED[0..4096]) == 0; + assert g_page("The compiler could not run in this browser."); +} + +test guesses_are_graded_remembered_in_the_address_and_checked { + g_open(); + w_hash_out(G_HASH[0..256]); + w_input(0, "100"); + w_click(5); + assert g_page("
  • 100"); + assert g_page_cp("aria-hidden=\"true\">", 226, 172, 134, "more, warm, within 50%
  • "); + assert g_page("5 of 6 guesses left."); + assert w_hash_out(G_HASH[0..256]) == 14; + assert same_bytes(G_HASH[0..14], 14, "#d=20730&g=100"); + assert w_value(0, G_OUT[0..10]) == 0; + assert w_effect(0, G_EFF[0..512]) == 6 + 1; + assert G_EFF[0] == 70; + assert same_bytes(G_EFF[1..7], 6, "#gt-in"); + w_input(0, "100"); + w_click(5); + assert g_page("

    You guessed that number already.

    "); + w_input(0, "1e3"); + w_click(5); + assert g_page("

    A whole number from 0 to 99999, please.

    "); + w_input(0, "-5"); + w_click(5); + assert g_page("A whole number from 0 to 99999, please."); + w_input(0, ""); + w_click(5); + w_input(0, "7.5"); + w_click(5); + assert g_page("5 of 6 guesses left."); + w_input(0, "1000"); + w_click(5); + assert g_page("
  • 1000"); + assert g_page("4 of 6 guesses left."); + assert g_page("

    "); +} + +test a_guess_within_ten_percent_ends_the_game_with_the_answer_and_a_share_text { + g_open(); + w_input(0, "100"); + w_click(5); + w_input(0, "190"); + w_click(5); + assert g_page("

    Within 10% in 2 of 6.

    yosys: 200 LUTs

    "); + assert g_page("

    Also 0 flip-flops, 1 CARRY4, 2 MUXF7, 3 MUXF8, 0 DSP48E1; 9 cells in all.

    "); + assert g_page("

    With synth_xilinx -family xc7 -flatten: the same LUT and DSP counts.

    "); + assert g_page("
    Every cell type yosys reported

    CARRY4 1, IBUF 2, LUT2 5

    "); + assert g_page("
    Gatle #1 2/6\n");
    +    var sh : [96]u8 = [0; 96];
    +    var k : u32 = raw(sh[0..96], 0, "Gatle #1 2/6");
    +    k = put(sh[0..96], k, 10);
    +    k = put(sh[0..96], k, 240);
    +    k = put(sh[0..96], k, 159);
    +    k = put(sh[0..96], k, 159);
    +    k = put(sh[0..96], k, 168);
    +    k = put(sh[0..96], k, 226);
    +    k = put(sh[0..96], k, 172);
    +    k = put(sh[0..96], k, 134);
    +    k = put(sh[0..96], k, 239);
    +    k = put(sh[0..96], k, 184);
    +    k = put(sh[0..96], k, 143);
    +    k = put(sh[0..96], k, 10);
    +    k = put(sh[0..96], k, 240);
    +    k = put(sh[0..96], k, 159);
    +    k = put(sh[0..96], k, 159);
    +    k = put(sh[0..96], k, 169);
    +    k = put(sh[0..96], k, 10);
    +    k = raw(sh[0..96], k, "t27.ai/widgets/gatle/
    "); + assert g_page(sh[0..(k as usize)]); + assert w_effect(0, G_EFF[0..512]) == 11; + assert G_EFF[0] == 110; + w_toggle(0, 1); + assert g_page("
    "); + // copy: the text and the selector go to the host; its answer shows and a timer clears it + w_click(5); + assert w_effect(0, G_EFF[0..512]) > 20; + assert G_EFF[0] == 119; + w_copied(1); + assert g_page("Copied."); + assert w_effect(0, G_EFF[0..512]) == 6; + assert G_EFF[0] == 116; + assert G_EFF[2] == 50; + assert G_EFF[3] == 53; + w_timer(G_EFF[1] as u32); + assert g_page(""); + w_click(5); + w_copied(0); + assert g_page("Copy failed; the text is selected, copy it by hand."); + // a draw in between: the answer of an older copy finds nothing to show + w_click(5); + w_click(1); + w_copied(1); + assert !g_page("Copied."); +} + +test six_wrong_guesses_lose_and_the_share_text_says_x { + g_open(); + var i : u32 = 0; + while (i < 6) { + var t : [8]u8 = [0; 8]; + const l : u32 = num(t[0..8], 0, (1000 + i * 7) as u64); + w_input(0, t[0..(l as usize)]); + w_click(5); + i = i + 1; + } + assert g_page("

    Out of guesses.

    "); + assert g_page("Gatle #1 X/6\n"); + assert g_page("0 of 6 guesses left.") == false; +} + +test the_address_is_read_the_way_url_search_params_reads_it { + g_open(); + w_hash_out(G_HASH[0..256]); + w_wall(W_G_DAY1 + 86400000.0 * 30.0); + w_hash("#d=20735&g=1.2.3"); + assert g_page("Gatle #6 of 2026-10-09, opened from a link. Today is #31."); + assert g_page("3"); + assert w_hash_out(G_HASH[0..256]) == 0; + w_hash("#%64=20731&g=%31%30.99999.100000.7"); + assert g_page("Gatle #2 of 2026-10-05"); + assert g_page("10"); + assert g_page("99999"); + assert !g_page("100000"); + assert g_page("7"); + w_hash("#d=20731&d=20732&g=5"); + assert g_page("Gatle #2 of 2026-10-05"); + w_hash("#p=1&g=5.6"); + assert g_page("Practice: a random module from the 2. Today's game is untouched."); + assert g_page("6"); + assert g_page("c/d"); + w_hash("#p=2"); + assert g_page("Gatle #31, 2026-11-03 UTC: puzzle 1 of 2."); + w_hash("#d=99999999"); + assert g_page("Gatle #31, 2026-11-03 UTC"); + w_hash("#d=20000"); + assert g_page("Gatle #31, 2026-11-03 UTC"); + w_hash("#g=1+2"); + assert g_page("Gatle #31, 2026-11-03 UTC"); + assert !g_page("gt-tile\">1"); +} + +test practice_deals_another_module_than_the_one_on_the_page { + g_open(); + w_rand(0.0); + w_click(1); + assert g_page("c/d"); + assert w_hash_out(G_HASH[0..256]) == 4; + assert same_bytes(G_HASH[0..4], 4, "#p=1"); + assert g_page("aria-pressed=\"true\">Practice"); + assert g_page(""); + w_rand(0.99); + w_click(2); + assert g_page("a/b"); + w_rand(0.99); + w_click(1); + assert g_page("c/d"); + w_rand(0.2); + w_click(2); + assert g_page("a/b"); + w_click(0); + assert g_page("a/b"); + assert w_hash_out(G_HASH[0..256]) == 8; +} + +test dates_are_written_the_way_to_iso_string_writes_them { + g_open(); + w_wall(W_G_DAY1 + 86400000.0 * 800.0); + w_hash("#d=20819"); + assert g_page("Gatle #90 of 2027-01-01"); + w_hash("#d=21000"); + assert g_page("Gatle #271 of 2027-07-01"); + w_wall(86400000.0 * 11017.0); + assert gt_today() == 20730; + w_wall(86400000.0 * 99999.0 + 5.0); + w_hash("#d=99999"); + assert g_page("Gatle #79270, 2243-10-16 UTC: puzzle "); + w_hash("#d=20730"); + assert g_page("Gatle #1 of 2026-10-04"); +} + +test the_links_of_the_source_follow_the_page_s_directory { + w_base("https://t27.ai/widgets/gatle/"); + w_wall(W_G_DAY1); + w_init(""); + w_data(0, W_G_POOL); + w_fetched(0, 1, W_G_SRC); + assert g_page("href=\"https://t27.ai/t27/files/specs/a/b.t27\""); + w_base("https://t27.ai/"); + assert g_page("href=\"https://t27.ai/t27/files/specs/a/b.t27\""); + w_base("https://x.y/a/b/c/"); + assert g_page("href=\"https://x.y/a/t27/files/specs/a/b.t27\""); +} + +test the_number_box_is_checked_the_way_a_browser_checks_it { + assert gt_box_valid("5"); + assert gt_box_valid("00042"); + assert gt_box_valid("1e3"); + assert gt_box_valid("1.0"); + assert gt_box_valid("-0"); + assert gt_box_valid("99999"); + assert gt_box_valid("1E2"); + assert gt_box_valid("12.50e1"); + assert !gt_box_valid(""); + assert !gt_box_valid("100000"); + assert !gt_box_valid("5.5"); + assert !gt_box_valid(".5"); + assert !gt_box_valid("-1"); + assert !gt_box_valid("1e"); + assert !gt_box_valid("abc"); + assert !gt_box_valid("5."); + assert !gt_box_valid("+5"); + assert !gt_box_valid("1e400"); +} + +test the_version_loses_its_bracketed_tail { + g_open(); + var v : [64]u8 = [0; 64]; + assert gt_version(v[0..64]) == 15; + assert same_bytes(v[0..15], 15, "Yosys 0.67+post"); +} diff --git a/specs/widgets/idcode.t27 b/specs/widgets/idcode.t27 new file mode 100644 index 0000000000..b8429dd000 --- /dev/null +++ b/specs/widgets/idcode.t27 @@ -0,0 +1,2207 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/idcode.t27 -- a JTAG IDCODE taken apart bit by bit, and named +; Source of truth for public/widgets/idcode/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English of its own: every +; word on screen is a SAY_ constant here, every number the tool relies on is a K_ constant. +; ASCII only (L3), English only (LANG-EN). + +; WHAT IT DOES: the reader types or pastes a 32-bit IDCODE (or a whole openFPGALoader printout; the +; first hex word is taken). The page draws the 32 bits as buttons in the four IEEE 1149.1 fields -- +; version [31:28], part number [27:12], manufacturer [11:1] and the fixed 1 in bit 0 -- splits the +; manufacturer into its JEP106 bank (continuation-code count plus one) and identity code, and then +; looks the code up the way openFPGALoader v1.1.1 does in src/jtag.cpp: the full IDCODE first, then +; with the version masked to 0. It shows the row it found (manufacturer, family, model, IR length), +; the lines `openFPGALoader --detect` would print for it, and a card the reader can save as a PNG. +; The URL hash keeps the code (#id=0x03636093), so a link reproduces the result. + +; DATA PROVENANCE: parts.json is extracted by scripts/widget-data/idcode.mjs from openFPGALoader's +; src/part.hpp and src/board.hpp at tag v1.1.1 (commit 85be4fa02b2d), sha256-pinned; it is the +; whole fpga_list, misc_dev_list and list_manufacturer (about 16 KB), plus the part of nine boards. +; The bench numbers come from openFPGALoader 1.1.1 run on this project's own bench. + +; WHAT THE BENCH SAID: on 2026-10-04 UTC one QMTech Wukong V1 was attached. With --verbose-level 2, +; openFPGALoader prints the raw word it shifted out: 0x13636093, version 1. Its --detect lines print +; idcode 0x3636093, because jtag.cpp stores the masked key it matched, not the raw word. So the +; 0x03636093 recorded for all three boards on 2026-08-17 (t27 fpga/HARDWARE_SSOT.md, W791) is the +; table key and the IDCODE our bitstreams carry; the raw word of the other two boards is not known +; to this page. + +; WHAT IT DOES NOT CLAIM: a name for a manufacturer code outside openFPGALoader's nine-entry list +; (the page gives the bank and code to look up in JEDEC JEP106 instead), a device for a code outside +; its table, or anything about a device beyond its table row. The table has its own slips, shown as +; they are: one row's manufacturer bits do not say Xilinx (0x04ac4033), keys listed twice keep only +; the first row, and the zynqmp_cfgn rows end in a 0 bit, which IEEE 1149.1 does not allow. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_idcode; + +use widgets::html_out::ok; +use widgets::html_out::OVER; +use widgets::html_out::put; +use widgets::html_out::raw; +use widgets::html_out::put_escaped; +use widgets::html_out::text; +use widgets::html_out::num; +use widgets::html_out::num_grouped; +use widgets::html_out::attr; +use widgets::html_out::fill; +use widgets::html_out::fill_named; +use widgets::html_out::dec_to; +use widgets::html_out::name_is; +use widgets::html_out::is_word_byte; +use widgets::html_out::H_LT; +use widgets::html_out::H_GT; +use widgets::html_out::H_AMP; +use widgets::html_out::H_QUOTE; +use widgets::html_out::H_APOS; +use widgets::html_out::H_SPACE; +use widgets::html_out::H_SLASH; +use widgets::html_out::H_EQ; +use widgets::json_scan::NO_SPAN; +use widgets::json_scan::SPAN; +use widgets::json_scan::J_NONE; +use widgets::json_scan::J_OBJECT; +use widgets::json_scan::J_ARRAY; +use widgets::json_scan::J_STRING; +use widgets::json_scan::J_NUMBER; +use widgets::json_scan::J_TRUE; +use widgets::json_scan::J_FALSE; +use widgets::json_scan::J_NULL; +use widgets::json_scan::jspan; +use widgets::json_scan::span_lo; +use widgets::json_scan::span_hi; +use widgets::json_scan::skip_ws; +use widgets::json_scan::string_end; +use widgets::json_scan::value_end; +use widgets::json_scan::root; +use widgets::json_scan::kind; +use widgets::json_scan::key_is; +use widgets::json_scan::obj_get; +use widgets::json_scan::arr_len; +use widgets::json_scan::arr_at; +use widgets::json_scan::hex4; +use widgets::json_scan::escape_at; +use widgets::json_scan::cp_utf8_len; +use widgets::json_scan::cp_utf8_byte; +use widgets::json_scan::str_len; +use widgets::json_scan::str_byte; +use widgets::js_num::JN_OVER; +use widgets::js_num::JN_CAP; +use widgets::js_num::JN_TWO52; +use widgets::js_num::JN_TWO53; +use widgets::js_num::jn_put; +use widgets::js_num::jn_raw; +use widgets::js_num::jn_digits; +use widgets::js_num::jn_mul; +use widgets::js_num::jn_mul2; +use widgets::js_num::jn_mul5; +use widgets::js_num::jn_shift; +use widgets::js_num::jn_cmp; +use widgets::js_num::jn_expo; +use widgets::js_num::jn_mant; +use widgets::js_num::jn_make; +use widgets::js_num::jn_exact; +use widgets::js_num::jn_fscale; +use widgets::js_num::js_is_nan; +use widgets::js_num::js_nan; +use widgets::js_num::js_inf; +use widgets::js_num::js_floor; +use widgets::js_num::js_ceil; +use widgets::js_num::js_max; +use widgets::js_num::js_min; +use widgets::js_num::js_round; +use widgets::js_num::jn_shortest; +use widgets::js_num::jn_copy; +use widgets::js_num::jn_put_run; +use widgets::js_num::jn_layout; +use widgets::js_num::js_str_num; +use widgets::js_num::js_to_fixed; +use widgets::js_num::js_locale_num; +use widgets::js_num::jn_num_ok; +use widgets::js_num::jn_cmp_text; +use widgets::js_num::js_parse_num; +use widgets::js_num::jn_dec_value; +use widgets::js_num::jn_abs; +use widgets::js_num::js_str_u64; +use widgets::js_val::jv_str_to; +use widgets::js_val::jv_text_str; +use widgets::js_val::jv_numval; +use widgets::js_val::jv_str; +use widgets::js_val::jv_text; +use widgets::js_val::jv_is; +use widgets::js_val::jv_truthy; +use widgets::js_val::jv_is_ws; +use widgets::js_val::jv_number_of_text; +use widgets::js_val::jv_num; +use widgets::js_val::jv_vput_str; +use widgets::js_val::jv_vput_f64; +use widgets::js_val::jv_vput_val; +use widgets::js_val::jv_vput_defined; +use widgets::js_val::jv_vput_fixed; +use widgets::js_val::jv_kid; +use widgets::js_val::jv_find; +use widgets::js_val::jv_fill; +use widgets::js_val::jv_string_ok; +use widgets::js_val::jv_number_ok; +use widgets::js_val::jv_lit_ok; +use widgets::js_val::jv_valid; +use widgets::js_val::jv_ws_at; +use widgets::js_val::jv_ws_before; +use widgets::js_val::jv_digit_value; +use widgets::js_val::jv_fill_raw; +use widgets::js_val::jv_fill_with; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "idcode"; +pub const TITLE : str = "JTAG IDCODE decoder: 32 bits, four fields, one device"; +pub const DESCRIPTION : str = "Paste a 32-bit JTAG IDCODE: version, part number, JEDEC manufacturer and the fixed 1 in bit 0 as clickable bits, then the device from openFPGALoader's own table."; +pub const IMAGE_ALT : str = "IDCODE card for 0x03636093: 32 bits in four coloured fields, version 0x0, part number 0x3636, manufacturer 0x049 (JEP106 bank 1, code 0x49, Xilinx) and bit 0 set to 1, found in openFPGALoader v1.1.1 as xc7a200, artix a7 200t, IR length 6. Our Wukong board reads 0x13636093 raw: version 1, same part."; +pub const HOOK : str = "Paste your board's IDCODE, get its bit fields and its device as a link: #id=0x03636093 is the Artix-7 200T on our bench."; +pub const CATEGORY : str = "fpga"; +pub const DATA_SOURCES : [7]str = ["openFPGALoader v1.1.1, github.com/trabucayre/openFPGALoader commit 85be4fa02b2dd6a83716d7dfac3d25bbd260ff7b, Apache-2.0, Copyright (C) 2019 Gwenhael Goavec-Merou: src/part.hpp (sha256 74ded91a5ef62d1f) fpga_list, misc_dev_list, list_manufacturer", "openFPGALoader v1.1.1 src/board.hpp (sha256 0c955d02921affa5): the FPGA part of each preset board", "openFPGALoader v1.1.1 src/jtag.cpp detectChain and src/main.cpp --detect: the lookup order (full IDCODE, then masked with 0x0fffffff) and the printed lines", "Bench 2026-10-04 UTC: openFPGALoader 1.1.1 -c digilent_hs2 --detect --verbose-level 2 on one QMTech Wukong V1 (XC7A200T): raw IDCODE 0x13636093, printed idcode 0x3636093", "t27 fpga/HARDWARE_SSOT.md, W791 2026-08-17: openFPGALoader 1.1.1 --detect on all three Wukong boards (--busdev-num 1:4, 1:6, 1:8) printed idcode 0x3636093, artix a7 200t, xc7a200", "IEEE Std 1149.1 device identification register: version [31:28], part number [27:12], manufacturer identity [11:1] per JEDEC JEP106, bit 0 always 1", "scripts/widget-data/idcode.mjs: writes public/widgets/idcode/parts.json from the pinned files and checks this spec's K_ numbers against the table and the bench record"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +; --- Numbers the tool relies on ------------------------------------------------------------ +pub const K_DATA_FILE : str = "./parts.json"; +pub const K_HASH_KEY : str = "id"; +; The four fields, most significant first: version, part number, manufacturer, fixed 1. +pub const K_FIELD_LSB : [4]u8 = [28, 12, 1, 0]; +pub const K_FIELD_WIDTH : [4]u8 = [4, 16, 11, 1]; +; Inside the 11-bit manufacturer field: the low 7 bits are the JEP106 identity code, the high 4 +; the number of 0x7F continuation codes before it (bank = that count + 1). +pub const K_MFR_CODE_BITS : u8 = 7; +; openFPGALoader's second lookup: the IDCODE with the version nibble cleared (0x0fffffff). +pub const K_VERSION_MASK : u32 = 268435455; +; 0x03636093, the fpga_list key for xc7a200 (artix a7 200t, IR length 6), and the IDCODE in the +; header of every bitstream we build for the XC7A200T. +pub const K_TABLE_IDCODE : u32 = 56844435; +pub const K_TABLE_PART : u16 = 13878; +pub const K_XILINX_MFR : u16 = 73; +pub const K_XC7A200T_IR_LENGTH : u8 = 6; +; 0x13636093, the raw word one Wukong board shifted out on 2026-10-04 UTC. +pub const K_BENCH_RAW_IDCODE : u32 = 325279891; +pub const K_BENCH_BOARDS_2026_08_17 : u8 = 3; +; What openFPGALoader treats specially: all zeros (TDO stuck low) and all ones (end of chain). +pub const K_STUCK_LOW : u32 = 0; +pub const K_END_OF_CHAIN : u32 = 4294967295; +; The PNG the reader can save. +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Words on screen ------------------------------------------------------------------------ +; Templates take {0}, {1}, ... in order. +pub const SAY_LOADING : str = "Reading openFPGALoader's device table"; +pub const SAY_LOAD_FAILED : str = "The device table did not load; the bit fields still work."; +pub const SAY_INPUT_LABEL : str = "IDCODE"; +pub const SAY_INPUT_PLACEHOLDER : str = "0x03636093, or paste openFPGALoader output"; +pub const SAY_BAD_INPUT : str = "No IDCODE here: type up to eight hex digits, like 0x03636093."; +pub const SAY_PRESETS_LABEL : str = "Try one"; +pub const SAY_BENCH_PRESET : str = "our bench, raw"; +pub const SAY_TABLE_PRESET : str = "our bench, as printed"; +pub const SAY_PRESET_TITLE : str = "openFPGALoader board {0}: part {1}"; +pub const SAY_BITS_LABEL : str = "Bits 31 to 0. Click a bit to flip it."; +pub const SAY_BIT_TITLE : str = "bit {0}: {1}"; +pub const SAY_FIELD_NAMES : [4]str = ["version", "part number", "manufacturer", "fixed 1"]; +pub const SAY_FIELD_BITS : [4]str = ["[31:28]", "[27:12]", "[11:1]", "[0]"]; +pub const SAY_COL_HEAD : [4]str = ["field", "bits", "hex", "decimal"]; +pub const SAY_BANK_LINE : str = "JEP106 bank {0} ({1} continuation codes), identity code {2}"; +pub const SAY_MFR_KNOWN : str = "{0} in openFPGALoader's manufacturer list"; +pub const SAY_MFR_UNKNOWN : str = "not in openFPGALoader's manufacturer list: look up bank {0}, code {1} in JEDEC JEP106"; +pub const SAY_BIT0_OK : str = "Bit 0 is 1, as IEEE 1149.1 requires of every IDCODE."; +pub const SAY_BIT0_BAD : str = "Bit 0 is 0: not an IDCODE. A TAP in BYPASS shifts out a 0 first, so a word read one bit late looks like this. Shifted right by one bit it reads {0}."; +pub const SAY_STUCK_LOW : str = "All zeros: openFPGALoader stops here with 'TDO is stuck at 0'."; +pub const SAY_END_OF_CHAIN : str = "All ones: what TDI looks like after the last device; openFPGALoader reads it as the end of the chain."; +pub const SAY_DEVICE_TITLE : str = "Device"; +pub const SAY_MATCH_EXACT : str = "In openFPGALoader v1.1.1's table as written."; +pub const SAY_MATCH_MASKED : str = "In openFPGALoader v1.1.1's table after the version is masked to 0, as openFPGALoader does: key {0}."; +pub const SAY_MATCH_MISC : str = "Not an FPGA: a TAP openFPGALoader knows and steps over."; +pub const SAY_MATCH_NONE : str = "Not in openFPGALoader v1.1.1's table, with or without the version."; +pub const SAY_DEVICE_FIELDS : [5]str = ["manufacturer", "family", "model", "IR length", "table key"]; +pub const SAY_MISC_FIELDS : [3]str = ["type", "IR length", "table key"]; +pub const SAY_SHADOWED : str = "part.hpp also lists {0} under this key; its std::map keeps only the first row."; +pub const SAY_MFR_MISMATCH : str = "The row says {0}, but its manufacturer bits say {1}."; +pub const SAY_DETECT_TITLE : str = "What openFPGALoader --detect prints"; +pub const SAY_DETECT_FPGA : [5]str = ["idcode 0x{0}", "manufacturer {0}", "family {0}", "model {0}", "irlength {0}"]; +pub const SAY_DETECT_MISC : [3]str = ["idcode 0x{0}", "type {0}", "irlength {0}"]; +pub const SAY_DETECT_NONE : str = "Unknown device with IDCODE: 0x{0} (manufacturer: 0x{1} ({2}), part: 0x{3} vers: 0x{4}"; +pub const SAY_DETECT_NOTE : str = "--detect prints the key it matched, so a masked version does not show. --verbose-level 2 prints the raw word."; +pub const SAY_BENCH_TITLE : str = "Our bench"; +pub const SAY_BENCH_LINE : str = "{0}: one QMTech Wukong V1 shifted out {1} (version {2}); openFPGALoader {3} --detect printed {4}."; +pub const SAY_BENCH_EARLIER : str = "On 2026-08-17 all {0} Wukong boards printed idcode 0x3636093 (t27 fpga/HARDWARE_SSOT.md). That line hides the version, so only this board's raw word is known."; +pub const SAY_COPY_LINK : str = "Copy link"; +pub const SAY_COPIED : str = "Link copied"; +pub const SAY_COPY_FAILED : str = "Copy it from the address bar"; +pub const SAY_SAVE_CARD : str = "Save card PNG"; +pub const SAY_CARD_TITLE : str = "JTAG IDCODE"; +pub const SAY_CARD_BRAND : str = "Trinity S3AI"; +pub const SAY_CARD_URL : str = "t27.ai/widgets/idcode/#id={0}"; +pub const SAY_CARD_NONE : str = "not in openFPGALoader's table"; +pub const SAY_CREDIT : str = "Device table: openFPGALoader {0}, commit {1}, Apache-2.0, Copyright (C) 2019 Gwenhael Goavec-Merou. {2} FPGA rows, {3} other TAPs, {4} manufacturer codes."; +pub const SAY_UNKNOWN : str = "unknown"; + +; --- Tests ---------------------------------------------------------------------------------- +test bit_zero_of_an_idcode_is_one { + assert K_FIELD_LSB[3] == 0; + assert K_FIELD_WIDTH[3] == 1; + assert K_TABLE_IDCODE % 2 == 1; + assert K_BENCH_RAW_IDCODE % 2 == 1; + assert K_STUCK_LOW % 2 == 0; +} + +test the_four_fields_tile_32_bits { + assert K_FIELD_WIDTH[0] + K_FIELD_WIDTH[1] + K_FIELD_WIDTH[2] + K_FIELD_WIDTH[3] == 32; + assert K_FIELD_LSB[0] == K_FIELD_LSB[1] + K_FIELD_WIDTH[1]; + assert K_FIELD_LSB[1] == K_FIELD_LSB[2] + K_FIELD_WIDTH[2]; + assert K_FIELD_LSB[2] == K_FIELD_LSB[3] + K_FIELD_WIDTH[3]; + assert K_FIELD_LSB[0] + K_FIELD_WIDTH[0] == 32; +} + +test x03636093_is_xilinx_0x049_part_0x3636 { + assert K_TABLE_IDCODE == 56844435; + assert K_XILINX_MFR == (K_TABLE_IDCODE / 2) % 2048; + assert K_XILINX_MFR == 73; + assert K_TABLE_PART == (K_TABLE_IDCODE / 4096) % 65536; + assert K_TABLE_PART == 13878; + assert K_TABLE_IDCODE / 268435456 == 0; + assert 147 != (K_TABLE_IDCODE / 2) % 2048; +} + +test xilinx_sits_in_jep106_bank_1_as_code_0x49 { + assert K_XILINX_MFR / 128 == 0; + assert K_XILINX_MFR % 128 == 73; + assert K_MFR_CODE_BITS + 4 == K_FIELD_WIDTH[2]; +} + +test the_bench_word_differs_from_the_table_key_only_in_its_version { + assert K_BENCH_RAW_IDCODE / 268435456 == 1; + assert K_BENCH_RAW_IDCODE % 268435456 == K_TABLE_IDCODE; + assert K_BENCH_RAW_IDCODE != K_TABLE_IDCODE; + assert K_VERSION_MASK + 1 == 268435456; + assert K_BENCH_BOARDS_2026_08_17 == 3; + assert K_XC7A200T_IR_LENGTH == 6; +} + +test all_ones_ends_the_chain_though_its_bit_0_is_1 { + assert K_END_OF_CHAIN % 2 == 1; + assert K_END_OF_CHAIN == 4294967295; +} + +test the_page_reads_nothing_and_sends_nothing { + assert READS_LOCAL_FILES == false; + assert SENDS_NOTHING == true; + assert same_text(CATEGORY, "fpga"); + assert same_text(K_HASH_KEY, "id"); +} + + +; ================================================================================================= +; THE NATIVE PAGE (epic gHashTag/t27#10937, issue #10938, stage "page" of specs/t27f/widget_factory.t27) +; Everything below replaces public/widgets/idcode/tool.js (270 lines of hand-written JavaScript). The +; words and numbers above are unchanged (but for two tests, see the end of this header). The page keeps +; its state in module-level `pub var`s and writes the page as HTML; the one shared host (host.js) sets +; it, forwards events and runs the effects. Entry points (all `pub fn`): +; w_init(hash) the page opens; hash is the address's #fragment bytes +; w_data(i, bytes) parts.json was fetched; w_data_failed(i) when it could not be +; w_input(i, text) the IDCODE field changed (i = 0) +; w_click(i) the i-th button in document order: the presets (2 + the rows of parts.json "presets"), +; then the 32 bits (bit 31 first), then "Copy link", then "Save card PNG" +; w_hash(hash) the address changed +; w_hash_out(dst) the address to show after the last event: byte 1, then "#id=0x03636093"; the host +; puts the origin and the path in front of it (tool.js writes origin + pathname + hash, +; so a query string is dropped, as there) +; w_value(i, dst) the text the IDCODE field must hold now (4294967295: leave it) +; w_render(dst) the page's HTML +; w_effect(k, dst) effects after an event: record 117 ("u") copy the link: byte 117, then the hash text +; the host appends to origin + pathname; record 80 ("P") save the card as a PNG: +; byte 80, then "1200\n630\nidcode-0x03636093.png" (see w_bdraw2); w_copied(ok) answers +; w_bdraw2(dst) the card as a BINARY draw list (the protocol of fasm_skyline.t27, plus three property +; codes: 36 textBaseline, 37 textAlign, 38 globalAlpha(f64); and a string that holds +; byte 1, a CSS variable name, byte 2, a fallback, byte 3 is replaced by the host with +; the variable of trimmed, or the fallback when it is empty: tool.js's +; `cs.getPropertyValue(v).trim() || fallback`, read when the card is saved) +; STATE tool.js keeps in DOM elements and in the closure, kept here as flags: `table` (parts.json indexed), +; and what each part of the page last showed. tool.js repaints the presets, the bench lines and the credit +; only when parts.json arrives, and the result at every code change. When parts.json is read halfway (a +; missing "source" throws after `table` is set), tool.js leaves the credit at "did not load" while the table +; stays in use, and the parts it did not reach keep their old look until the next change of the code. P_TBL, +; B_TBL, R_TBL and P_PR are those looks. A card the page has not repainted shows what the code was when it +; was painted, which is the current code: the code changes only through set(), which repaints. +; Not carried over from JavaScript (said plainly; the measurement does not cover them): +; - a row of parts.json, or an element of "presets", that is a STRING is read by JavaScript as its characters +; (r[0] is the first character); here it has no cells. A value of "raw"/"printed" that is a string or a +; number reads here as a throw; JavaScript reads r[0] of a string. +; - an array as a preset label (el() flattens it), a preset label that is an array; "commit" that is an array; +; - a manufacturer or model cell that is not a string where tool.js calls .toLowerCase() on it (JavaScript +; throws halfway through draw(); here the text is compared as String(cell)); +; - toLowerCase() folds ASCII only; a cell text is cut at W_CELL_CAP bytes; parts.json above W_DATA_CAP bytes +; is "did not load"; a duplicate key inside a JSON object reads as the first here, the last in JSON.parse; +; - decodeURIComponent throws on a malformed %-escape or on bytes that are not UTF-8; here a malformed escape +; reads as "no code in the address" and bytes are not checked; +; - parseInt of more than 53 bits of hex digits rounds step by step, not as V8 does; a 12-unit commit cut that +; would split a surrogate pair is cut before the pair; +; - the address: tool.js's replaceState is called once per change, so is this page's, after the event. + +; MEASURED 2026-10-19: t27c gen + a 12-line Zig export wrapper + zig build-exe wasm32-freestanding ReleaseSmall give a +; 39368-byte wasm. The original page (tool.js) and this one ran in two DOM windows (happy-dom) with a FAKE getContext('2d') +; that records every method call and every property assignment (the card canvas is never in the page: its sequence is +; carried in the text of the downloaded file); after every action the DOM (the input value too), the address, the saved +; files (name, type, the whole recorded call sequence of the card), the clipboard and the blob log were compared: +; 7446 comparisons, 0 differences. They cover the real parts.json with every control (the 11 presets, the 32 bits, copy +; with an allowed, a refused and a missing clipboard, save PNG for 190 codes), every key of the table typed plain and with +; a version, every manufacturer, 10 addresses at load, 12 address changes, 2400 fuzzed typings (hex with and without 0x, +; arrow lines, white space of every kind, underscores, noise, table keys) with bit clicks, saves and address changes, +; 48 variants of parts.json (empty lists, presets missing / null / odd / not iterable, bench missing / odd / throwing, +; source missing / odd, rows that make indexTable throw, duplicate keys, empty and null names, markup and unicode, hex forms +; of every kind, numbers as keys, minified and escaped JSON, truncated JSON), a 404, a non-JSON answer and events before +; the data arrives. Negative controls: a changed DOM node, a changed input value, a rect digit moved by 1e-9, a property +; value moved by 1e-9, a skipped call, a changed CSS fallback, a renamed download, a copy without the hash and a skipped +; address write are each reported. NOT compared (happy-dom has no layout, canvas or clipboard): the pixels, the bytes of +; the PNG, the real clipboard, focus. + +pub const W_DATA_CAP : u32 = 262144; +pub const W_CELL_CAP : u32 = 4096; +pub const W_MONO_VAR : str = "--w-mono"; +pub const W_MONO_FB : str = "monospace"; +pub const W_GREEN_VAR : str = "--w-green"; +pub const W_GREEN_FB : str = "#00ff88"; +pub const W_GOLD_VAR : str = "--w-gold"; +pub const W_GOLD_FB : str = "#ffd700"; +pub const W_TEXT_VAR : str = "--w-text"; +pub const W_TEXT_FB : str = "#e8fff7"; +pub const W_MUTED_VAR : str = "--w-subtle"; +pub const W_MUTED_FB : str = "#8fa9a0"; +pub const W_RED_VAR : str = "--w-red"; +pub const W_RED_FB : str = "#ff4d6d"; +pub const W_BLUE : str = "#6cb6ff"; +pub const W_FIELD_CLASS : [4]str = ["idc-f-ver", "idc-f-part", "idc-f-mfr", "idc-f-one"]; +pub const W_FIELD_DIGITS : [4]u8 = [1, 4, 3, 1]; + +pub var DATA : [262144]u8 = [0; 262144]; +pub var DLEN : u32 = 0; +pub var CODE : u32 = 0; +pub var MSG : u8 = 0; +pub var SHARE : u8 = 0; +pub var WRITE_IN : bool = false; +pub var HASH_PEND : bool = false; +pub var CREDIT : u8 = 0; +pub var TBL : bool = false; +pub var P_TBL : bool = false; +pub var B_TBL : bool = false; +pub var R_TBL : bool = false; +pub var P_PR : bool = false; +pub var EF : [4]u8 = [0; 4]; +pub var EF_N : u32 = 0; +pub var H_KIND : u8 = 0; +pub var H_KEY : u32 = 0; +pub var H_MASKED : bool = false; +pub var H_ROW : i64 = -1; +pub var DV : [4]u32 = [0; 4]; +pub var D_MFR : u32 = 0; +pub var D_CONT : u32 = 0; +pub var D_ID : u32 = 0; +pub var D_NAME : i64 = -1; + +// --- small pieces ----------------------------------------------------------------------------------- + +fn hexv(b: u8) -> i64 { + if (b >= 48 and b <= 57) { return (b - 48) as i64; } + if (b >= 97 and b <= 102) { return (b - 97) as i64 + 10; } + if (b >= 65 and b <= 70) { return (b - 65) as i64 + 10; } + return -1; +} + +/// 2 ** k. +fn pow2(k: u32) -> u64 { + var r : u64 = 1; + var i : u32 = 0; + while (i < k) { + r = r * 2; + i = i + 1; + } + return r; +} + +/// hexN / printf_x: the lowercase hex digits of v, at least min of them, "0x" in front when prefix; written +/// to o. A word of o that does not fit returns OVER. +fn put_hex(o: []u8, n0: u32, v: u32, min: u32, prefix: bool) -> u32 { + var n : u32 = n0; + if (prefix) { + n = put(o, n, 48); + n = put(o, n, 120); + } + var count : u32 = 1; + var t : u32 = v / 16; + while (t > 0) { + count = count + 1; + t = t / 16; + } + var width : u32 = count; + if (min > width) { width = min; } + var k : u32 = width; + while (k > 0) { + k = k - 1; + var d : u32 = v; + var sh : u32 = 0; + while (sh < k) { + d = d / 16; + sh = sh + 1; + } + const nib : u32 = d % 16; + if (nib < 10) { n = put(o, n, (nib as u8) + 48); } else { n = put(o, n, (nib as u8) + 87); } + } + return n; +} + +/// The hex text of v as a byte string in tmp; its length. +fn hex_to(tmp: []u8, v: u32, min: u32, prefix: bool) -> u32 { + const l : u32 = put_hex(tmp, 0, v, min, prefix); + if (l == OVER) { return 0; } + return l; +} + +/// parseInt(text, 16) >>> 0 for the text b: white space, a sign, an optional 0x, the hex digits; no digit is NaN, which +/// is 0 after the shift. +fn parse_int16(b: []const u8) -> u32 { + const len : u32 = b.len as u32; + var i : u32 = 0; + while (i < len) { + const w : u32 = jv_ws_at(b, i, len); + if (w == 0) { break; } + i = i + w; + } + var neg : bool = false; + if (i < len and (b[i as usize] == 43 or b[i as usize] == 45)) { + if (b[i as usize] == 45) { neg = true; } + i = i + 1; + } + if (i + 1 < len and b[i as usize] == 48 and (b[(i + 1) as usize] == 120 or b[(i + 1) as usize] == 88)) { i = i + 2; } + var val : f64 = 0.0; + var cnt : u32 = 0; + while (i < len) { + const h : i64 = hexv(b[i as usize]); + if (h < 0) { break; } + val = val * 16.0 + (h as f64); + cnt = cnt + 1; + i = i + 1; + } + if (cnt == 0) { return 0; } + if (neg) { val = 0.0 - val; } + if (val - val != 0.0) { return 0; } + const m : f64 = val - js_floor(val / 4294967296.0) * 4294967296.0; + return m as u32; +} + +/// Is the value of the JSON text at sp present for `??`: not missing and not null. +fn present(d: []const u8, sp: i64) -> bool { + const k : u8 = kind(d, sp); + return k != J_NONE and k != J_NULL; +} + +/// parseInt(String(v), 16) >>> 0 of the value at sp. +fn int16_of(d: []const u8, sp: i64) -> u32 { + var tmp : [4096]u8 = [0; 4096]; + const l : u32 = jv_str(tmp[0..], 0, d, sp); + if (l == OVER) { return 0; } + return parse_int16(tmp[0..(l as usize)]); +} + +/// The i-th element of the row at sp when the row is an array; a row of another kind has no cells. +fn cell(d: []const u8, row: i64, i: u32) -> i64 { + if (kind(d, row) != J_ARRAY) { return NO_SPAN; } + return arr_at(d, row, i); +} + +/// String(v) of the value at sp into tmp (cut at the end of tmp); its length. +fn str_of(tmp: []u8, d: []const u8, sp: i64) -> u32 { + const l : u32 = jv_str(tmp, 0, d, sp); + if (l == OVER) { return 0; } + return l; +} + +/// A child of el(): missing and null add nothing, the rest is String(v) as escaped text. +fn put_kid(o: []u8, n0: u32, d: []const u8, sp: i64) -> u32 { + if (!present(d, sp)) { return n0; } + return jv_text(o, n0, d, sp); +} + +/// String(v) as escaped text (the template literal and fill() forms). +fn put_val(o: []u8, n0: u32, d: []const u8, sp: i64) -> u32 { + return jv_text(o, n0, d, sp); +} + +fn is_space_only(t: []const u8) -> bool { + const len : u32 = t.len as u32; + var i : u32 = 0; + while (i < len) { + const w : u32 = jv_ws_at(t, i, len); + if (w == 0) { return false; } + i = i + w; + } + return true; +} + +fn count_hex_run(t: []const u8, p: usize) -> usize { + var e : usize = p; + while (e < t.len and hexv(t[e]) >= 0) { e = e + 1; } + return e; +} + +fn run_value(t: []const u8, p: usize, e: usize) -> u64 { + var v : u64 = 0; + var i : usize = p; + while (i < e) { + v = v * 16 + (hexv(t[i]) as u64); + i = i + 1; + } + return v; +} + +/// The regex tail 0x([0-9a-f]{1,8})\b at q: the end of the match (the hex run) or 0 when it does not match; the +/// value is left in M_VAL. +pub var M_VAL : u64 = 0; + +fn match_0x(t: []const u8, q: usize) -> usize { + if (q + 1 >= t.len) { return 0; } + if (t[q] != 48) { return 0; } + if (t[q + 1] != 120 and t[q + 1] != 88) { return 0; } + const p : usize = q + 2; + const e : usize = count_hex_run(t, p); + if (e == p or e - p > 8) { return 0; } + if (e < t.len and is_word_byte(t[e])) { return 0; } + M_VAL = run_value(t, p, e); + return e; +} + +/// parseCode(text): openFPGALoader's raw line (-> 0x13636093, the first one that is not 0xffffffff), then the +/// first 0x word, then a bare run of up to eight hex digits; -1 when there is none. +fn parse_code(t: []const u8) -> i64 { + const len : u32 = t.len as u32; + var p : usize = 0; + while (p + 1 < t.len) { + if (t[p] == 45 and t[p + 1] == 62) { + var q : usize = p + 2; + while (q < t.len) { + const w : u32 = jv_ws_at(t, q as u32, len); + if (w == 0) { break; } + q = q + (w as usize); + } + const e : usize = match_0x(t, q); + if (e > 0) { + if (M_VAL != 4294967295) { return M_VAL as i64; } + p = e; + } else { + p = p + 1; + } + } else { + p = p + 1; + } + } + p = 0; + while (p + 1 < t.len) { + const e : usize = match_0x(t, p); + if (e > 0) { return M_VAL as i64; } + p = p + 1; + } + var bare : [64]u8 = [0; 64]; + var bn : usize = 0; + var i : u32 = 0; + while (i < len) { + const w : u32 = jv_ws_at(t, i, len); + if (w > 0) { + i = i + w; + } else { + if (t[i as usize] == 95) { + i = i + 1; + } else { + if (bn >= 64) { return -1; } + bare[bn] = t[i as usize]; + bn = bn + 1; + i = i + 1; + } + } + } + if (bn < 1 or bn > 8) { return -1; } + var k : usize = 0; + var v : u64 = 0; + while (k < bn) { + const h : i64 = hexv(bare[k]); + if (h < 0) { return -1; } + v = v * 16 + (h as u64); + k = k + 1; + } + return v as i64; +} + +/// codeFromHash(): the first [#&]id=value (value: one or more bytes up to the next &) of the address, percent-decoded +/// and read by parseCode; -1 when there is none. +fn code_from_hash(h: []const u8) -> i64 { + const key : []const u8 = K_HASH_KEY; + var p : usize = 0; + while (p < h.len) { + if (h[p] == 35 or h[p] == 38) { + var ok_key : bool = p + 1 + key.len < h.len; + var k : usize = 0; + while (ok_key and k < key.len) { + if (h[p + 1 + k] != key[k]) { ok_key = false; } + k = k + 1; + } + if (ok_key and h[p + 1 + key.len] == 61) { + const a : usize = p + 2 + key.len; + var e : usize = a; + while (e < h.len and h[e] != 38) { e = e + 1; } + if (e > a) { + var dec : [256]u8 = [0; 256]; + var dn : usize = 0; + var i : usize = a; + while (i < e) { + if (dn >= 256) { return -1; } + if (h[i] == 37) { + if (i + 2 >= e) { return -1; } + const hi : i64 = hexv(h[i + 1]); + const lo : i64 = hexv(h[i + 2]); + if (hi < 0 or lo < 0) { return -1; } + dec[dn] = (hi * 16 + lo) as u8; + i = i + 3; + } else { + dec[dn] = h[i]; + i = i + 1; + } + dn = dn + 1; + } + return parse_code(dec[0..dn]); + } + } + } + p = p + 1; + } + return -1; +} + +// --- the table: tool.js's indexTable and lookup, read from the bytes of parts.json ---------------------- + +fn tbl(d: []const u8, name: []const u8) -> i64 { + return obj_get(d, root(d), name); +} + +/// Is the value at arr an array none of whose elements is null (r[0] of a null row throws)? +fn rows_ok(d: []const u8, arr: i64) -> bool { + if (kind(d, arr) != J_ARRAY) { return false; } + const hi : usize = span_hi(arr); + var p : usize = skip_ws(d, span_lo(arr) + 1); + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + if (kind(d, jspan(p, e)) == J_NULL) { return false; } + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + return true; +} + +/// indexTable(d) does not throw: d is an object whose fpga, misc, manufacturers and shadowed are arrays of non-null rows. +fn table_ok(d: []const u8) -> bool { + if (kind(d, root(d)) != J_OBJECT) { return false; } + if (!rows_ok(d, tbl(d, "fpga"))) { return false; } + if (!rows_ok(d, tbl(d, "misc"))) { return false; } + if (!rows_ok(d, tbl(d, "manufacturers"))) { return false; } + return rows_ok(d, tbl(d, "shadowed")); +} + +/// The last row of the array at arr whose key (parseInt of its cell 0, as unsigned) is `key`: what new Map(rows) +/// keeps; NO_SPAN when there is none. +fn find_row(d: []const u8, arr: i64, key: u32) -> i64 { + var res : i64 = NO_SPAN; + const hi : usize = span_hi(arr); + var p : usize = skip_ws(d, span_lo(arr) + 1); + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + const r : i64 = jspan(p, e); + if (int16_of(d, cell(d, r, 0)) == key) { res = r; } + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + return res; +} + +/// table.mfr.get(key): the second cell of the last manufacturer row of that key (NO_SPAN when there is none). +fn mfr_get(d: []const u8, key: u32) -> i64 { + const r : i64 = find_row(d, tbl(d, "manufacturers"), key); + if (r == NO_SPAN) { return NO_SPAN; } + return cell(d, r, 1); +} + +/// lookup(code) with a table (use_table) or without: H_KIND 0 is the null of no table, 1 an fpga row, 2 a misc +/// row, 3 {kind: 'none'}. +fn do_lookup(c: u32, use_table: bool) -> u32 { + H_KIND = 0; + H_ROW = NO_SPAN; + H_MASKED = false; + H_KEY = 0; + if (!use_table) { return 0; } + H_KIND = 3; + const d : []const u8 = DATA[0..(DLEN as usize)]; + var pass : u32 = 0; + while (pass < 2) { + var key : u32 = c; + if (pass == 1) { key = c % 268435456; } + const f : i64 = find_row(d, tbl(d, "fpga"), key); + if (f != NO_SPAN) { + H_KIND = 1; + H_KEY = key; + H_ROW = f; + H_MASKED = key != c; + return 0; + } + const m : i64 = find_row(d, tbl(d, "misc"), key); + if (m != NO_SPAN) { + H_KIND = 2; + H_KEY = key; + H_ROW = m; + H_MASKED = key != c; + return 0; + } + pass = pass + 1; + } + return 0; +} + +fn field_of(c: u32, i: u32) -> u32 { + const lsb : u32 = K_FIELD_LSB[i as usize] as u32; + const width : u32 = K_FIELD_WIDTH[i as usize] as u32; + return (((c as u64) / pow2(lsb)) % pow2(width)) as u32; +} + +/// The field index a bit belongs to: LSB.findIndex((l, i) => bit >= l && bit < l + WIDTH[i]). +fn field_index(bit: u32) -> u32 { + var i : u32 = 0; + while (i < 4) { + const l : u32 = K_FIELD_LSB[i as usize] as u32; + const w : u32 = K_FIELD_WIDTH[i as usize] as u32; + if (bit >= l and bit < l + w) { return i; } + i = i + 1; + } + return 4294967295; +} + +/// decode(code): the four fields and the manufacturer split; the name from the table when use_table. +fn decode(c: u32, use_table: bool) -> u32 { + var i : u32 = 0; + while (i < 4) { + DV[i as usize] = field_of(c, i); + i = i + 1; + } + D_MFR = DV[2]; + const cb : u32 = pow2(K_MFR_CODE_BITS as u32) as u32; + D_CONT = D_MFR / cb; + D_ID = D_MFR % cb; + D_NAME = NO_SPAN; + if (use_table) { + const d : []const u8 = DATA[0..(DLEN as usize)]; + const nm : i64 = mfr_get(d, D_MFR); + if (present(d, nm)) { D_NAME = nm; } + } + return 0; +} + +// --- state: tool.js's set(), the events, the data ------------------------------------------------------- + +fn set_code(c: u32, write_input: bool) -> u32 { + CODE = c; + if (write_input) { + WRITE_IN = true; + MSG = 0; + } + HASH_PEND = true; + SHARE = 0; + R_TBL = TBL; + P_PR = true; + return 0; +} + +pub fn w_init(h: []const u8) -> u32 { + DLEN = 0; + CREDIT = 0; + TBL = false; + P_TBL = false; + B_TBL = false; + R_TBL = false; + P_PR = false; + MSG = 0; + SHARE = 0; + EF_N = 0; + const v : i64 = code_from_hash(h); + var start : u32 = K_TABLE_IDCODE; + if (v >= 0) { start = v as u32; } + set_code(start, false); + WRITE_IN = true; + return 0; +} + +pub fn w_input(i: u32, s: []const u8) -> u32 { + if (i != 0) { return 0; } + const v : i64 = parse_code(s); + if (v < 0) { + MSG = 0; + if (!is_space_only(s)) { MSG = 1; } + return 0; + } + MSG = 0; + set_code(v as u32, false); + return 0; +} + +pub fn w_hash(h: []const u8) -> u32 { + const v : i64 = code_from_hash(h); + if (v >= 0 and (v as u32) != CODE) { set_code(v as u32, true); } + return 0; +} + +pub fn w_hash_out(dst: []u8) -> u32 { + if (!HASH_PEND) { return 0; } + HASH_PEND = false; + var n : u32 = put(dst, 0, 1); + n = raw(dst, n, "#"); + n = raw(dst, n, K_HASH_KEY); + n = raw(dst, n, "="); + n = put_hex(dst, n, CODE, 8, true); + if (n == OVER) { return 0; } + return n; +} + +pub fn w_value(i: u32, dst: []u8) -> u32 { + if (i != 0 or !WRITE_IN) { return 4294967295; } + WRITE_IN = false; + const n : u32 = put_hex(dst, 0, CODE, 8, true); + if (n == OVER) { return 4294967295; } + return n; +} + +/// The number of buttons in the presets row: the two constants, and the rows of parts.json "presets" once painted. +fn preset_count() -> u32 { + if (!P_TBL) { return 2; } + const d : []const u8 = DATA[0..(DLEN as usize)]; + const p : i64 = tbl(d, "presets"); + if (kind(d, p) != J_ARRAY) { return 2; } + return 2 + arr_len(d, p); +} + +pub fn w_click(i: u32) -> u32 { + const np : u32 = preset_count(); + if (i < np) { + if (i == 0) { + set_code(K_BENCH_RAW_IDCODE, true); + } else { + if (i == 1) { + set_code(K_TABLE_IDCODE, true); + } else { + const d : []const u8 = DATA[0..(DLEN as usize)]; + const row : i64 = arr_at(d, tbl(d, "presets"), i - 2); + set_code(int16_of(d, cell(d, row, 2)), true); + } + } + return 0; + } + if (i < np + 32) { + const b : u32 = 31 - (i - np); + const flip : u32 = pow2(b) as u32; + var next : u32 = CODE + flip; + if ((CODE / flip) % 2 == 1) { next = CODE - flip; } + set_code(next, true); + return 0; + } + if (i == np + 32) { + EF[0] = 117; + EF_N = 1; + return 0; + } + if (i == np + 33) { + EF[0] = 80; + EF_N = 1; + } + return 0; +} + +pub fn w_copied(ok_copy: u32) -> u32 { + SHARE = 2; + if (ok_copy != 0) { SHARE = 1; } + return 0; +} + +pub fn w_data_failed(i: u32) -> u32 { + CREDIT = 2; + return 0; +} + +/// The credit text's "commit.slice(0, 12)": the first 12 UTF-16 units of the decoded string at sp, as bytes. +fn commit12(tmp: []u8, d: []const u8, sp: i64) -> u32 { + var full : [512]u8 = [0; 512]; + var fl : u32 = jv_str(full[0..], 0, d, sp); + if (fl == OVER) { fl = 0; } + var i : u32 = 0; + var units : u32 = 0; + while (i < fl) { + const b : u8 = full[i as usize]; + var w : u32 = 1; + var u : u32 = 1; + if (b >= 192) { w = 2; } + if (b >= 224) { w = 3; } + if (b >= 240) { + w = 4; + u = 2; + } + if (units + u > 12) { break; } + units = units + u; + i = i + w; + } + var k : u32 = 0; + while (k < i and k < fl) { + tmp[k as usize] = full[k as usize]; + k = k + 1; + } + return i; +} + +pub fn w_data(i: u32, s: []const u8) -> u32 { + CREDIT = 2; + if (s.len > (W_DATA_CAP as usize)) { return 0; } + var k : usize = 0; + while (k < s.len) { + DATA[k] = s[k]; + k = k + 1; + } + DLEN = s.len as u32; + const d : []const u8 = DATA[0..(DLEN as usize)]; + if (!jv_valid(d)) { return 0; } + if (!table_ok(d)) { return 0; } + TBL = true; + // d.source.tag, d.source.commit.slice(0, 12) + const src : i64 = tbl(d, "source"); + if (kind(d, src) != J_OBJECT) { return 0; } + if (kind(d, obj_get(d, src, "commit")) != J_STRING) { return 0; } + CREDIT = 1; + // drawPresets(): for (const [board, part, hex] of presets ?? []) + const pr : i64 = tbl(d, "presets"); + const pk : u8 = kind(d, pr); + if (pk != J_NONE and pk != J_NULL) { + if (pk != J_ARRAY) { + CREDIT = 2; + return 0; + } + const hi : usize = span_hi(pr); + var p : usize = skip_ws(d, span_lo(pr) + 1); + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + const rk : u8 = kind(d, jspan(p, e)); + if (rk == J_NULL or rk == J_NUMBER or rk == J_TRUE or rk == J_FALSE or rk == J_OBJECT) { + CREDIT = 2; + return 0; + } + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + } + P_TBL = true; + P_PR = false; + // drawBench(): b.raw[0], b.printed[0], b.date.replace, b.tool.replace + const b : i64 = tbl(d, "bench"); + if (jv_truthy(d, b)) { + if (kind(d, b) != J_OBJECT) { + CREDIT = 2; + return 0; + } + if (kind(d, obj_get(d, b, "raw")) != J_ARRAY or kind(d, obj_get(d, b, "printed")) != J_ARRAY) { + CREDIT = 2; + return 0; + } + if (kind(d, obj_get(d, b, "date")) != J_STRING or kind(d, obj_get(d, b, "tool")) != J_STRING) { + CREDIT = 2; + return 0; + } + } + B_TBL = true; + // draw() + R_TBL = true; + P_PR = true; + return 0; +} + +pub fn w_effect(k: u32, dst: []u8) -> u32 { + if (k >= EF_N) { + EF_N = 0; + return 0; + } + var n : u32 = 0; + if (EF[k as usize] == 117) { + n = put(dst, 0, 117); + n = raw(dst, n, "#"); + n = raw(dst, n, K_HASH_KEY); + n = raw(dst, n, "="); + n = put_hex(dst, n, CODE, 8, true); + } else { + n = put(dst, 0, 80); + n = num(dst, n, K_CARD_WIDTH as u64); + n = put(dst, n, 10); + n = num(dst, n, K_CARD_HEIGHT as u64); + n = put(dst, n, 10); + n = raw(dst, n, ID); + n = raw(dst, n, "-"); + n = put_hex(dst, n, CODE, 8, true); + n = raw(dst, n, ".png"); + } + if (n == OVER) { return 0; } + return n; +} + +// --- the page --------------------------------------------------------------------------------------------- + +/// One preset button: . +fn put_preset(o: []u8, n0: u32, label: []const u8, kid: bool, d: []const u8, label_sp: i64, value: u32, title: []const u8) -> u32 { + var n : u32 = raw(o, n0, ""); +} + +fn put_presets(o: []u8, n0: u32) -> u32 { + var n : u32 = n0; + var tmp : [16]u8 = [0; 16]; + const tb : u32 = hex_to(tmp[0..], K_BENCH_RAW_IDCODE, 8, true); + n = put_preset(o, n, SAY_BENCH_PRESET, false, "", NO_SPAN, K_BENCH_RAW_IDCODE, tmp[0..(tb as usize)]); + const tt : u32 = hex_to(tmp[0..], K_TABLE_IDCODE, 8, true); + n = put_preset(o, n, SAY_TABLE_PRESET, false, "", NO_SPAN, K_TABLE_IDCODE, tmp[0..(tt as usize)]); + if (!P_TBL) { return n; } + const d : []const u8 = DATA[0..(DLEN as usize)]; + const pr : i64 = tbl(d, "presets"); + if (kind(d, pr) != J_ARRAY) { return n; } + const hi : usize = span_hi(pr); + var p : usize = skip_ws(d, span_lo(pr) + 1); + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + const row : i64 = jspan(p, e); + var vb : [8192]u8 = [0; 8192]; + var vn : u32 = jv_vput_val(vb[0..], 0, "0", d, cell(d, row, 0)); + vn = jv_vput_val(vb[0..], vn, "1", d, cell(d, row, 1)); + var title : [8192]u8 = [0; 8192]; + var tl : u32 = jv_fill_raw(title[0..], 0, SAY_PRESET_TITLE, vb[0..], vn, true); + if (tl == OVER) { tl = 0; } + n = put_preset(o, n, "", true, d, cell(d, row, 0), int16_of(d, cell(d, row, 2)), title[0..(tl as usize)]); + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + return n; +} + +fn put_bits(o: []u8, n0: u32) -> u32 { + var n : u32 = n0; + var b : u32 = 32; + while (b > 0) { + b = b - 1; + const f : u32 = field_index(b); + const on : bool = (CODE / (pow2(b) as u32)) % 2 == 1; + n = raw(o, n, ""); + } + return n; +} + +/// A

    ...

    whose text is fill(template, a, b, c). +fn put_line(o: []u8, n0: u32, cls: []const u8, t: []const u8, a: []const u8, b: []const u8, c: []const u8, k: u32) -> u32 { + var n : u32 = raw(o, n0, "

    "); + n = fill(o, n, t, a, b, c, k); + return raw(o, n, "

    "); +} + +fn put_fields_table(o: []u8, n0: u32) -> u32 { + var n : u32 = raw(o, n0, ""); + var h : usize = 0; + while (h < 4) { + n = raw(o, n, ""); + h = h + 1; + } + n = raw(o, n, ""); + var i : u32 = 0; + while (i < 4) { + n = raw(o, n, ""); + i = i + 1; + } + return raw(o, n, "
    "); + n = text(o, n, SAY_COL_HEAD[h]); + n = raw(o, n, "
    "); + n = text(o, n, SAY_FIELD_NAMES[i as usize]); + n = raw(o, n, ""); + n = text(o, n, SAY_FIELD_BITS[i as usize]); + n = raw(o, n, ""); + if (i == 3) { n = num(o, n, DV[3] as u64); } else { n = put_hex(o, n, DV[i as usize], W_FIELD_DIGITS[i as usize] as u32, true); } + n = raw(o, n, ""); + n = num(o, n, DV[i as usize] as u64); + n = raw(o, n, "
    "); +} + +/// The text of fill(template, String(v)) for a JSON value, as escaped text. +fn put_fill1(o: []u8, n0: u32, t: []const u8, d: []const u8, sp: i64) -> u32 { + var vb : [8192]u8 = [0; 8192]; + const vn : u32 = jv_vput_val(vb[0..], 0, "0", d, sp); + if (vn == OVER) { return OVER; } + return jv_fill(o, n0, t, vb[0..], vn, true); +} + +/// Does the String(a) lowercased contain the String(b) lowercased (ASCII). +fn lower_byte(b: u8) -> u8 { + if (b >= 65 and b <= 90) { return b + 32; } + return b; +} + +fn contains_ci(hay: []const u8, nee: []const u8) -> bool { + if (nee.len == 0) { return true; } + if (nee.len > hay.len) { return false; } + var i : usize = 0; + while (i + nee.len <= hay.len) { + var k : usize = 0; + while (k < nee.len and lower_byte(hay[i + k]) == lower_byte(nee[k])) { k = k + 1; } + if (k == nee.len) { return true; } + i = i + 1; + } + return false; +} + +fn put_device(o: []u8, n0: u32, d: []const u8) -> u32 { + if (H_KIND == 0) { return n0; } + var n : u32 = raw(o, n0, "

    "); + n = text(o, n, SAY_DEVICE_TITLE); + n = raw(o, n, "

    "); + if (H_KIND == 3) { + n = raw(o, n, "

    "); + n = text(o, n, SAY_MATCH_NONE); + return raw(o, n, "

    "); + } + n = raw(o, n, "

    "); + if (H_KIND == 2) { + n = text(o, n, SAY_MATCH_MISC); + } else { + if (H_MASKED) { n = put_fill1(o, n, SAY_MATCH_MASKED, d, cell(d, H_ROW, 0)); } else { n = text(o, n, SAY_MATCH_EXACT); } + } + n = raw(o, n, "

    "); + if (H_KIND == 1) { + var i : u32 = 0; + while (i < 5) { + n = raw(o, n, "
    "); + n = text(o, n, SAY_DEVICE_FIELDS[i as usize]); + n = raw(o, n, "
    "); + if (i == 0) { n = put_kid(o, n, d, cell(d, H_ROW, 1)); } + if (i == 1) { n = put_kid(o, n, d, cell(d, H_ROW, 2)); } + if (i == 2) { n = put_kid(o, n, d, cell(d, H_ROW, 3)); } + if (i == 3) { n = put_val(o, n, d, cell(d, H_ROW, 4)); } + if (i == 4) { n = put_kid(o, n, d, cell(d, H_ROW, 0)); } + n = raw(o, n, "
    "); + i = i + 1; + } + } else { + var i : u32 = 0; + while (i < 3) { + n = raw(o, n, "
    "); + n = text(o, n, SAY_MISC_FIELDS[i as usize]); + n = raw(o, n, "
    "); + if (i == 0) { n = put_kid(o, n, d, cell(d, H_ROW, 1)); } + if (i == 1) { n = put_val(o, n, d, cell(d, H_ROW, 2)); } + if (i == 2) { n = put_kid(o, n, d, cell(d, H_ROW, 0)); } + n = raw(o, n, "
    "); + i = i + 1; + } + } + n = raw(o, n, "
    "); + if (H_KIND == 1) { + // the row's own manufacturer against what its key's manufacturer bits say + const key_mfr : u32 = field_of(H_KEY, 2); + const named : i64 = mfr_get(d, key_mfr); + const row1 : i64 = cell(d, H_ROW, 1); + var bad : bool = !jv_truthy(d, named); + if (!bad) { + var a : [4096]u8 = [0; 4096]; + var b : [4096]u8 = [0; 4096]; + const al : u32 = str_of(a[0..], d, named); + const bl : u32 = str_of(b[0..], d, row1); + bad = !contains_ci(a[0..(al as usize)], b[0..(bl as usize)]); + } + if (bad) { + var vb : [8192]u8 = [0; 8192]; + var vn : u32 = jv_vput_val(vb[0..], 0, "0", d, row1); + if (present(d, named)) { + vn = jv_vput_val(vb[0..], vn, "1", d, named); + } else { + var hx : [16]u8 = [0; 16]; + const hl : u32 = hex_to(hx[0..], key_mfr, 3, true); + vn = jv_vput_str(vb[0..], vn, "1", hx[0..(hl as usize)]); + } + n = raw(o, n, "

    "); + if (vn != OVER) { n = jv_fill(o, n, SAY_MFR_MISMATCH, vb[0..], vn, true); } else { n = OVER; } + n = raw(o, n, "

    "); + } + // shadowed rows listed under this key + const sh : i64 = tbl(d, "shadowed"); + const hi : usize = span_hi(sh); + var p : usize = skip_ws(d, span_lo(sh) + 1); + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + const r : i64 = jspan(p, e); + if (int16_of(d, cell(d, r, 0)) == H_KEY) { + n = raw(o, n, "

    "); + n = put_fill1(o, n, SAY_SHADOWED, d, cell(d, r, 3)); + n = raw(o, n, "

    "); + } + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + } + return raw(o, n, ""); +} + +/// One line of the --detect block: fill(template, String(v)) where v is already text. +fn put_detect_line(o: []u8, n0: u32, t: []const u8, v: []const u8, first: bool) -> u32 { + var n : u32 = n0; + if (!first) { n = put(o, n, 10); } + return fill(o, n, t, v, "", "", 1); +} + +fn put_detect(o: []u8, n0: u32, d: []const u8) -> u32 { + if (H_KIND == 0 or CODE == K_STUCK_LOW or CODE == K_END_OF_CHAIN) { return n0; } + var n : u32 = raw(o, n0, "

    "); + n = text(o, n, SAY_DETECT_TITLE); + n = raw(o, n, "

    ");
    +    var kx : [16]u8 = [0; 16];
    +    if (H_KIND == 1) {
    +        const kl : u32 = hex_to(kx[0..], H_KEY, 0, false);
    +        n = put_detect_line(o, n, SAY_DETECT_FPGA[0], kx[0..(kl as usize)], true);
    +        var i : u32 = 1;
    +        while (i < 5) {
    +            var cv : [4096]u8 = [0; 4096];
    +            var cl : u32 = 0;
    +            if (i == 1) { cl = str_of(cv[0..], d, cell(d, H_ROW, 1)); }
    +            if (i == 2) { cl = str_of(cv[0..], d, cell(d, H_ROW, 2)); }
    +            if (i == 3) { cl = str_of(cv[0..], d, cell(d, H_ROW, 3)); }
    +            if (i == 4) { cl = str_of(cv[0..], d, cell(d, H_ROW, 4)); }
    +            n = put_detect_line(o, n, SAY_DETECT_FPGA[i as usize], cv[0..(cl as usize)], false);
    +            i = i + 1;
    +        }
    +    }
    +    if (H_KIND == 2) {
    +        const kl : u32 = hex_to(kx[0..], H_KEY, 0, false);
    +        n = put_detect_line(o, n, SAY_DETECT_MISC[0], kx[0..(kl as usize)], true);
    +        var i : u32 = 1;
    +        while (i < 3) {
    +            var cv : [4096]u8 = [0; 4096];
    +            var cl : u32 = 0;
    +            if (i == 1) { cl = str_of(cv[0..], d, cell(d, H_ROW, 1)); }
    +            if (i == 2) { cl = str_of(cv[0..], d, cell(d, H_ROW, 2)); }
    +            n = put_detect_line(o, n, SAY_DETECT_MISC[i as usize], cv[0..(cl as usize)], false);
    +            i = i + 1;
    +        }
    +    }
    +    if (H_KIND == 3) {
    +        // jtag.cpp's message for an unknown code: %08x, %03x, the manufacturer name ("" when absent), IDCODE2PART as %02x, IDCODE2VERS as %x
    +        var vb : [8192]u8 = [0; 8192];
    +        var t0 : [16]u8 = [0; 16];
    +        var t1 : [16]u8 = [0; 16];
    +        var t3 : [16]u8 = [0; 16];
    +        var t4 : [16]u8 = [0; 16];
    +        const l0 : u32 = hex_to(t0[0..], CODE, 8, false);
    +        const l1 : u32 = hex_to(t1[0..], D_MFR, 3, false);
    +        const part7 : u32 = ((CODE as u64) / pow2(21) % 128) as u32;
    +        const l3 : u32 = hex_to(t3[0..], part7, 2, false);
    +        const l4 : u32 = hex_to(t4[0..], DV[0], 0, false);
    +        var vn : u32 = jv_vput_str(vb[0..], 0, "0", t0[0..(l0 as usize)]);
    +        vn = jv_vput_str(vb[0..], vn, "1", t1[0..(l1 as usize)]);
    +        if (D_NAME != NO_SPAN) { vn = jv_vput_val(vb[0..], vn, "2", d, D_NAME); } else { vn = jv_vput_str(vb[0..], vn, "2", ""); }
    +        vn = jv_vput_str(vb[0..], vn, "3", t3[0..(l3 as usize)]);
    +        vn = jv_vput_str(vb[0..], vn, "4", t4[0..(l4 as usize)]);
    +        if (vn != OVER) { n = jv_fill(o, n, SAY_DETECT_NONE, vb[0..], vn, true); } else { n = OVER; }
    +    }
    +    n = raw(o, n, "

    "); + n = text(o, n, SAY_DETECT_NOTE); + return raw(o, n, "

    "); +} + +/// draw(): the result card for the code, with the table when the page had it at the last repaint. +fn put_result(o: []u8, n0: u32) -> u32 { + do_lookup(CODE, R_TBL); + decode(CODE, R_TBL); + const d : []const u8 = DATA[0..(DLEN as usize)]; + var n : u32 = raw(o, n0, "

    "); + n = put_hex(o, n, CODE, 8, true); + n = raw(o, n, "

    "); + if (H_KIND == 1) { + n = put_val(o, n, d, cell(d, H_ROW, 3)); + n = put(o, n, 32); + n = put(o, n, 194); + n = put(o, n, 183); + n = put(o, n, 32); + n = put_val(o, n, d, cell(d, H_ROW, 2)); + } + if (H_KIND == 2) { n = put_kid(o, n, d, cell(d, H_ROW, 1)); } + if (H_KIND == 3) { n = text(o, n, SAY_CARD_NONE); } + n = raw(o, n, "

    "); + n = put_fields_table(o, n); + var bank : [16]u8 = [0; 16]; + var cont : [16]u8 = [0; 16]; + var idh : [16]u8 = [0; 16]; + const bl : u32 = dec_to(bank[0..], (D_CONT + 1) as u64); + const cl : u32 = dec_to(cont[0..], D_CONT as u64); + const il : u32 = hex_to(idh[0..], D_ID, 2, true); + n = put_line(o, n, "idc-line idc-f-mfr", SAY_BANK_LINE, bank[0..(bl as usize)], cont[0..(cl as usize)], idh[0..(il as usize)], 3); + if (D_NAME != NO_SPAN and jv_truthy(d, D_NAME)) { + n = raw(o, n, "

    "); + n = put_fill1(o, n, SAY_MFR_KNOWN, d, D_NAME); + n = raw(o, n, "

    "); + } else { + n = put_line(o, n, "idc-line", SAY_MFR_UNKNOWN, bank[0..(bl as usize)], idh[0..(il as usize)], "", 2); + } + if (DV[3] == 1) { + n = put_line(o, n, "idc-line idc-ok", SAY_BIT0_OK, "", "", "", 0); + } else { + var sh : [16]u8 = [0; 16]; + const sl : u32 = hex_to(sh[0..], CODE / 2, 8, true); + n = put_line(o, n, "idc-line idc-warn", SAY_BIT0_BAD, sh[0..(sl as usize)], "", "", 1); + } + if (CODE == K_STUCK_LOW) { n = put_line(o, n, "idc-line idc-warn", SAY_STUCK_LOW, "", "", "", 0); } + if (CODE != K_STUCK_LOW and CODE == K_END_OF_CHAIN) { n = put_line(o, n, "idc-line idc-warn", SAY_END_OF_CHAIN, "", "", "", 0); } + n = put_device(o, n, d); + n = put_detect(o, n, d); + return n; +} + +/// drawBench(): the heading, the bench line once the table has said what it was, and the earlier boards. +fn put_bench(o: []u8, n0: u32) -> u32 { + var n : u32 = raw(o, n0, "

    "); + n = text(o, n, SAY_BENCH_TITLE); + n = raw(o, n, "

    "); + if (B_TBL) { + const d : []const u8 = DATA[0..(DLEN as usize)]; + const b : i64 = tbl(d, "bench"); + if (jv_truthy(d, b)) { + const raw_v : u32 = int16_of(d, arr_at(d, obj_get(d, b, "raw"), 0)); + const printed : u32 = int16_of(d, arr_at(d, obj_get(d, b, "printed"), 0)); + var vb : [8192]u8 = [0; 8192]; + // date.replace('T', ' '): the first T only + var date : [4096]u8 = [0; 4096]; + const dl : u32 = str_of(date[0..], d, obj_get(d, b, "date")); + var di : u32 = 0; + var done : bool = false; + while (di < dl) { + if (!done and date[di as usize] == 84) { + date[di as usize] = 32; + done = true; + } + di = di + 1; + } + var vn : u32 = jv_vput_str(vb[0..], 0, "0", date[0..(dl as usize)]); + var h8 : [16]u8 = [0; 16]; + const hl : u32 = hex_to(h8[0..], raw_v, 8, true); + vn = jv_vput_str(vb[0..], vn, "1", h8[0..(hl as usize)]); + var ver : [16]u8 = [0; 16]; + const vl : u32 = dec_to(ver[0..], (raw_v / 268435456) as u64); + vn = jv_vput_str(vb[0..], vn, "2", ver[0..(vl as usize)]); + // tool.replace(/^openFPGALoader\s*/, '') + var tool : [4096]u8 = [0; 4096]; + const tl : u32 = str_of(tool[0..], d, obj_get(d, b, "tool")); + var ts : u32 = 0; + const pre : []const u8 = "openFPGALoader"; + var same : bool = tl >= (pre.len as u32); + var pk : usize = 0; + while (same and pk < pre.len) { + if (tool[pk] != pre[pk]) { same = false; } + pk = pk + 1; + } + if (same) { + ts = pre.len as u32; + while (ts < tl) { + const w : u32 = jv_ws_at(tool[0..(tl as usize)], ts, tl); + if (w == 0) { break; } + ts = ts + w; + } + } + vn = jv_vput_str(vb[0..], vn, "3", tool[(ts as usize)..(tl as usize)]); + var pt : [24]u8 = [0; 24]; + pt[0] = 48; + pt[1] = 120; + const pl : u32 = put_hex(pt[0..], 2, printed, 0, false); + vn = jv_vput_str(vb[0..], vn, "4", pt[0..(pl as usize)]); + n = raw(o, n, "

    "); + if (vn != OVER) { n = jv_fill(o, n, SAY_BENCH_LINE, vb[0..], vn, true); } else { n = OVER; } + n = raw(o, n, "

    "); + } + } + n = raw(o, n, "

    "); + var c3 : [8]u8 = [0; 8]; + const cl3 : u32 = dec_to(c3[0..], K_BENCH_BOARDS_2026_08_17 as u64); + n = fill(o, n, SAY_BENCH_EARLIER, c3[0..(cl3 as usize)], "", "", 1); + return raw(o, n, "

    "); +} + +fn put_credit(o: []u8, n0: u32) -> u32 { + if (CREDIT == 0) { return text(o, n0, SAY_LOADING); } + if (CREDIT == 2) { return text(o, n0, SAY_LOAD_FAILED); } + const d : []const u8 = DATA[0..(DLEN as usize)]; + const src : i64 = tbl(d, "source"); + var vb : [8192]u8 = [0; 8192]; + var vn : u32 = jv_vput_val(vb[0..], 0, "0", d, obj_get(d, src, "tag")); + var cm : [64]u8 = [0; 64]; + const cl : u32 = commit12(cm[0..], d, obj_get(d, src, "commit")); + vn = jv_vput_str(vb[0..], vn, "1", cm[0..(cl as usize)]); + var c2 : [16]u8 = [0; 16]; + var l2 : u32 = dec_to(c2[0..], arr_len(d, tbl(d, "fpga")) as u64); + vn = jv_vput_str(vb[0..], vn, "2", c2[0..(l2 as usize)]); + l2 = dec_to(c2[0..], arr_len(d, tbl(d, "misc")) as u64); + vn = jv_vput_str(vb[0..], vn, "3", c2[0..(l2 as usize)]); + l2 = dec_to(c2[0..], arr_len(d, tbl(d, "manufacturers")) as u64); + vn = jv_vput_str(vb[0..], vn, "4", c2[0..(l2 as usize)]); + if (vn == OVER) { return OVER; } + return jv_fill(o, n0, SAY_CREDIT, vb[0..], vn, true); +} + +pub fn w_render(dst: []u8) -> u32 { + var n : u32 = raw(dst, 0, "

    "); + if (MSG == 1) { n = text(dst, n, SAY_BAD_INPUT); } + n = raw(dst, n, "

    "); + n = text(dst, n, SAY_PRESETS_LABEL); + n = raw(dst, n, "
    "); + n = put_presets(dst, n); + n = raw(dst, n, "

    "); + n = text(dst, n, SAY_BITS_LABEL); + n = raw(dst, n, "

    "); + n = put_bits(dst, n); + n = raw(dst, n, "
    "); + var i : u32 = 0; + while (i < 4) { + n = raw(dst, n, ""); + n = text(dst, n, SAY_FIELD_NAMES[i as usize]); + n = raw(dst, n, " "); + n = text(dst, n, SAY_FIELD_BITS[i as usize]); + n = raw(dst, n, ""); + i = i + 1; + } + n = raw(dst, n, "
    "); + n = put_result(dst, n); + n = raw(dst, n, "
    "); + n = put_bench(dst, n); + n = raw(dst, n, "

    "); + n = put_credit(dst, n); + n = raw(dst, n, "

    "); + if (n == OVER) { return 0; } + return n; +} + +// --- the card the reader can save: a binary draw list (see the header) -------------------------------------- + +fn bw_b(o: []u8, n: u32, b: u8) -> u32 { + if (n == OVER or (n as usize) >= o.len) { return OVER; } + o[n as usize] = b; + return n + 1; +} + +/// The IEEE 754 bits of a finite double (no subnormals, no NaN). +fn f64_bits(x: f64) -> u64 { + if (x == 0.0) { + if ((1.0 / x) < 0.0) { return 9223372036854775808; } + return 0; + } + var sign : u64 = 0; + var ax : f64 = x; + if (x < 0.0) { + sign = 9223372036854775808; + ax = 0.0 - x; + } + const m : u64 = jn_mant(ax); + const e : i32 = jn_expo(ax); + const be : u64 = ((e + 52 + 1023) as u64); + return sign + be * 4503599627370496 + (m - 4503599627370496); +} + +fn bw_f(o: []u8, n0: u32, x: f64) -> u32 { + var bits : u64 = f64_bits(x); + var n : u32 = n0; + var k : u32 = 0; + while (k < 8) { + n = bw_b(o, n, (bits % 256) as u8); + bits = bits / 256; + k = k + 1; + } + return n; +} + +/// A string: u16 length, then the bytes. +fn bw_s(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = bw_b(o, n0, (s.len % 256) as u8); + n = bw_b(o, n, ((s.len / 256) % 256) as u8); + var k : usize = 0; + while (k < s.len) { + n = bw_b(o, n, s[k]); + k = k + 1; + } + return n; +} + +fn bw_len(o: []u8, n0: u32, len: u32) -> u32 { + const n : u32 = bw_b(o, n0, (len % 256) as u8); + return bw_b(o, n, ((len / 256) % 256) as u8); +} + +fn bw_raw(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = n0; + var k : usize = 0; + while (k < s.len) { + n = bw_b(o, n, s[k]); + k = k + 1; + } + return n; +} + +/// The bytes of a CSS variable with a fallback: 1, name, 2, fallback, 3. +fn bw_varbytes(o: []u8, n0: u32, name: []const u8, fb: []const u8) -> u32 { + var n : u32 = bw_b(o, n0, 1); + n = bw_raw(o, n, name); + n = bw_b(o, n, 2); + n = bw_raw(o, n, fb); + return bw_b(o, n, 3); +} + +/// A colour property (op 32 fillStyle, 33 strokeStyle) set to a CSS variable with its fallback. +fn bw_color(o: []u8, n0: u32, op: u8, name: []const u8, fb: []const u8) -> u32 { + var n : u32 = bw_b(o, n0, op); + n = bw_len(o, n, (name.len as u32) + (fb.len as u32) + 3); + return bw_varbytes(o, n, name, fb); +} + +/// A property set to a plain string. +fn bw_set_s(o: []u8, n0: u32, op: u8, v: []const u8) -> u32 { + const n : u32 = bw_b(o, n0, op); + return bw_s(o, n, v); +} + +/// g.font = `${weight}${px}px ${mono}`: prefix is "700 26px ". +fn bw_font(o: []u8, n0: u32, prefix: []const u8) -> u32 { + var n : u32 = bw_b(o, n0, 35); + n = bw_len(o, n, (prefix.len as u32) + (W_MONO_VAR.len as u32) + (W_MONO_FB.len as u32) + 3); + n = bw_raw(o, n, prefix); + return bw_varbytes(o, n, W_MONO_VAR, W_MONO_FB); +} + +fn bw_set_f(o: []u8, n0: u32, op: u8, v: f64) -> u32 { + const n : u32 = bw_b(o, n0, op); + return bw_f(o, n, v); +} + +fn bw_rect(o: []u8, n0: u32, op: u8, a: f64, b: f64, c: f64, d: f64) -> u32 { + var n : u32 = bw_b(o, n0, op); + n = bw_f(o, n, a); + n = bw_f(o, n, b); + n = bw_f(o, n, c); + return bw_f(o, n, d); +} + +fn bw_pt(o: []u8, n0: u32, op: u8, x: f64, y: f64) -> u32 { + var n : u32 = bw_b(o, n0, op); + n = bw_f(o, n, x); + return bw_f(o, n, y); +} + +fn bw_text(o: []u8, n0: u32, s: []const u8, x: f64, y: f64) -> u32 { + var n : u32 = bw_b(o, n0, 13); + n = bw_s(o, n, s); + n = bw_f(o, n, x); + return bw_f(o, n, y); +} + +/// The colour of field f: version gold, part number green, manufacturer blue, the fixed 1 text. +fn bw_field_color(o: []u8, n0: u32, op: u8, f: u32) -> u32 { + if (f == 0) { return bw_color(o, n0, op, W_GOLD_VAR, W_GOLD_FB); } + if (f == 1) { return bw_color(o, n0, op, W_GREEN_VAR, W_GREEN_FB); } + if (f == 2) { return bw_set_s(o, n0, op, W_BLUE); } + return bw_color(o, n0, op, W_TEXT_VAR, W_TEXT_FB); +} + +/// SAY_CARD_BRAND with its first word-bounded S3AI written S, superscript three, AI. +fn brand_to(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = n0; + var i : usize = 0; + var done : bool = false; + while (i < s.len) { + if (!done and i + 4 <= s.len and s[i] == 83 and s[i + 1] == 51 and s[i + 2] == 65 and s[i + 3] == 73) { + const before : bool = i == 0 or !is_word_byte(s[i - 1]); + const after : bool = i + 4 == s.len or !is_word_byte(s[i + 4]); + if (before and after) { + n = put(o, n, 83); + n = put(o, n, 194); + n = put(o, n, 179); + n = put(o, n, 65); + n = put(o, n, 73); + i = i + 4; + done = true; + } else { + n = put(o, n, s[i]); + i = i + 1; + } + } else { + n = put(o, n, s[i]); + i = i + 1; + } + } + return n; +} + +/// The card: what tool.js's saveCard() draws on its 1200x630 canvas, call for call. +pub fn w_bdraw2(o: []u8) -> u32 { + const wd : f64 = K_CARD_WIDTH as f64; + const ht : f64 = K_CARD_HEIGHT as f64; + const use_table : bool = TBL; + do_lookup(CODE, use_table); + decode(CODE, use_table); + const d : []const u8 = DATA[0..(DLEN as usize)]; + var n : u32 = bw_set_s(o, 0, 32, "#000"); + n = bw_rect(o, n, 10, 0.0, 0.0, wd, ht); + n = bw_color(o, n, 32, W_GREEN_VAR, W_GREEN_FB); + n = bw_font(o, n, "700 26px "); + n = bw_set_s(o, n, 36, "top"); + n = bw_b(o, n, 1); + n = bw_pt(o, n, 7, 60.0, 52.0); + n = bw_pt(o, n, 8, 84.0, 52.0); + n = bw_pt(o, n, 8, 72.0, 72.0); + n = bw_b(o, n, 2); + n = bw_b(o, n, 3); + var brand : [128]u8 = [0; 128]; + const bl : u32 = brand_to(brand[0..], 0, SAY_CARD_BRAND); + n = bw_text(o, n, brand[0..(bl as usize)], 98.0, 48.0); + n = bw_set_s(o, n, 37, "right"); + n = bw_color(o, n, 32, W_MUTED_VAR, W_MUTED_FB); + n = bw_font(o, n, "22px "); + n = bw_text(o, n, SAY_CARD_TITLE, wd - 60.0, 52.0); + n = bw_set_s(o, n, 37, "left"); + n = bw_set_s(o, n, 32, "#fff"); + n = bw_font(o, n, "800 92px "); + var hx : [16]u8 = [0; 16]; + const hl : u32 = hex_to(hx[0..], CODE, 8, true); + n = bw_text(o, n, hx[0..(hl as usize)], 60.0, 104.0); + // the model line + var model : [8192]u8 = [0; 8192]; + var ml : u32 = 0; + if (H_KIND == 1) { + var t : [4096]u8 = [0; 4096]; + var tl : u32 = str_of(t[0..], d, cell(d, H_ROW, 3)); + ml = jn_raw(model[0..], 0, t[0..(tl as usize)]); + ml = put(model[0..], ml, 32); + ml = put(model[0..], ml, 194); + ml = put(model[0..], ml, 183); + ml = put(model[0..], ml, 32); + tl = str_of(t[0..], d, cell(d, H_ROW, 2)); + ml = jn_raw(model[0..], ml, t[0..(tl as usize)]); + ml = put(model[0..], ml, 32); + ml = put(model[0..], ml, 194); + ml = put(model[0..], ml, 183); + ml = put(model[0..], ml, 32); + ml = jn_raw(model[0..], ml, SAY_DEVICE_FIELDS[3]); + ml = put(model[0..], ml, 32); + tl = str_of(t[0..], d, cell(d, H_ROW, 4)); + ml = jn_raw(model[0..], ml, t[0..(tl as usize)]); + } + if (H_KIND == 2) { ml = str_of(model[0..], d, cell(d, H_ROW, 1)); } + if (H_KIND == 0 or H_KIND == 3) { ml = jn_raw(model[0..], 0, SAY_CARD_NONE); } + if (H_KIND != 0 and H_KIND != 3) { + n = bw_color(o, n, 32, W_GREEN_VAR, W_GREEN_FB); + } else { + n = bw_color(o, n, 32, W_MUTED_VAR, W_MUTED_FB); + } + n = bw_font(o, n, "700 30px "); + n = bw_text(o, n, model[0..(ml as usize)], 60.0, 214.0); + // 32 bit cells + const x0 : f64 = 60.0; + const cw : f64 = (wd - 120.0) / 32.0; + const y0 : f64 = 290.0; + const ch : f64 = 70.0; + var b : u32 = 32; + while (b > 0) { + b = b - 1; + const i : u32 = 31 - b; + const f : u32 = field_index(b); + const on : bool = (CODE / (pow2(b) as u32)) % 2 == 1; + const x : f64 = x0 + (i as f64) * cw; + if (on) { n = bw_field_color(o, n, 32, f); } else { n = bw_set_s(o, n, 32, "#0b1210"); } + if (on) { n = bw_set_f(o, n, 38, 0.9); } else { n = bw_set_f(o, n, 38, 1.0); } + n = bw_rect(o, n, 10, x + 2.0, y0, cw - 4.0, ch); + n = bw_set_f(o, n, 38, 1.0); + if (b == 0 and !on) { n = bw_color(o, n, 33, W_RED_VAR, W_RED_FB); } else { n = bw_field_color(o, n, 33, f); } + n = bw_set_f(o, n, 34, 2.0); + n = bw_rect(o, n, 11, x + 2.0, y0, cw - 4.0, ch); + if (on) { n = bw_set_s(o, n, 32, "#000"); } else { n = bw_field_color(o, n, 32, f); } + n = bw_font(o, n, "700 28px "); + n = bw_set_s(o, n, 37, "center"); + if (on) { n = bw_text(o, n, "1", x + cw / 2.0, y0 + 20.0); } else { n = bw_text(o, n, "0", x + cw / 2.0, y0 + 20.0); } + n = bw_color(o, n, 32, W_MUTED_VAR, W_MUTED_FB); + n = bw_font(o, n, "14px "); + var bt : [8]u8 = [0; 8]; + const btl : u32 = dec_to(bt[0..], b as u64); + n = bw_text(o, n, bt[0..(btl as usize)], x + cw / 2.0, y0 + ch + 8.0); + } + // field captions under their bits + var f : u32 = 0; + while (f < 4) { + const lsb : f64 = (K_FIELD_LSB[f as usize] as u32) as f64; + const wdt : f64 = (K_FIELD_WIDTH[f as usize] as u32) as f64; + const left : f64 = x0 + (31.0 - (lsb + wdt - 1.0)) * cw; + const right : f64 = x0 + (32.0 - lsb) * cw; + n = bw_field_color(o, n, 33, f); + n = bw_set_f(o, n, 34, 2.0); + n = bw_b(o, n, 1); + n = bw_pt(o, n, 7, left + 4.0, y0 + ch + 34.0); + n = bw_pt(o, n, 8, right - 4.0, y0 + ch + 34.0); + n = bw_b(o, n, 4); + if (f == 3) { n = bw_set_s(o, n, 37, "right"); } else { n = bw_set_s(o, n, 37, "center"); } + var cx : f64 = (left + right) / 2.0; + if (f == 3) { cx = right; } + n = bw_field_color(o, n, 32, f); + n = bw_font(o, n, "700 22px "); + var cap : [16]u8 = [0; 16]; + var cl : u32 = 0; + if (f == 3) { cl = dec_to(cap[0..], DV[3] as u64); } else { cl = hex_to(cap[0..], DV[f as usize], W_FIELD_DIGITS[f as usize] as u32, true); } + n = bw_text(o, n, cap[0..(cl as usize)], cx, y0 + ch + 44.0); + n = bw_color(o, n, 32, W_MUTED_VAR, W_MUTED_FB); + n = bw_font(o, n, "16px "); + if (f != 3) { n = bw_text(o, n, SAY_FIELD_NAMES[f as usize], cx, y0 + ch + 72.0); } + f = f + 1; + } + n = bw_set_s(o, n, 37, "left"); + n = bw_color(o, n, 32, W_TEXT_VAR, W_TEXT_FB); + n = bw_font(o, n, "22px "); + // fill(SAY_BANK_LINE, bank, cont, hexN(id, 2)) and the manufacturer name + var line : [8192]u8 = [0; 8192]; + var bankv : [16]u8 = [0; 16]; + var contv : [16]u8 = [0; 16]; + var idv : [16]u8 = [0; 16]; + const bkl : u32 = dec_to(bankv[0..], (D_CONT + 1) as u64); + const cnl : u32 = dec_to(contv[0..], D_CONT as u64); + const idl : u32 = hex_to(idv[0..], D_ID, 2, true); + var ll : u32 = fill_raw3(line[0..], 0, SAY_BANK_LINE, bankv[0..(bkl as usize)], contv[0..(cnl as usize)], idv[0..(idl as usize)], 3); + if (D_NAME != NO_SPAN and jv_truthy(d, D_NAME)) { + ll = put(line[0..], ll, 32); + ll = put(line[0..], ll, 194); + ll = put(line[0..], ll, 183); + ll = put(line[0..], ll, 32); + var nm : [4096]u8 = [0; 4096]; + const nl : u32 = str_of(nm[0..], d, D_NAME); + ll = jn_raw(line[0..], ll, nm[0..(nl as usize)]); + } + n = bw_text(o, n, line[0..(ll as usize)], 60.0, 500.0); + if (DV[3] == 1) { n = bw_color(o, n, 32, W_MUTED_VAR, W_MUTED_FB); } else { n = bw_color(o, n, 32, W_RED_VAR, W_RED_FB); } + n = bw_font(o, n, "18px "); + var b0 : [8192]u8 = [0; 8192]; + var b0l : u32 = 0; + if (DV[3] == 1) { + b0l = jn_raw(b0[0..], 0, SAY_BIT0_OK); + } else { + var sh : [16]u8 = [0; 16]; + const shl : u32 = hex_to(sh[0..], CODE / 2, 8, true); + b0l = fill_raw3(b0[0..], 0, SAY_BIT0_BAD, sh[0..(shl as usize)], "", "", 1); + } + if (b0l > 92) { + b0l = 89; + b0l = jn_raw(b0[0..], b0l, "..."); + } + n = bw_text(o, n, b0[0..(b0l as usize)], 60.0, 534.0); + n = bw_color(o, n, 32, W_GREEN_VAR, W_GREEN_FB); + n = bw_font(o, n, "20px "); + var url : [256]u8 = [0; 256]; + const ul : u32 = fill_raw3(url[0..], 0, SAY_CARD_URL, hx[0..(hl as usize)], "", "", 1); + n = bw_text(o, n, url[0..(ul as usize)], 60.0, 580.0); + if (n == OVER) { return 0; } + return n; +} + +/// fill(template, a, b, c) as plain bytes: {0} {1} {2} replaced by the first k values, other braces as written. +fn fill_raw3(o: []u8, n0: u32, t: []const u8, a: []const u8, b: []const u8, c: []const u8, k: u32) -> u32 { + var n : u32 = n0; + var i : usize = 0; + while (i < t.len) { + if (t[i] == 123 and i + 2 < t.len and t[i + 2] == 125 and t[i + 1] >= 48 and t[i + 1] <= 50 and ((t[i + 1] - 48) as u32) < k) { + const w : u8 = t[i + 1] - 48; + if (w == 0) { n = jn_raw(o, n, a); } + if (w == 1) { n = jn_raw(o, n, b); } + if (w == 2) { n = jn_raw(o, n, c); } + i = i + 3; + } else { + n = put(o, n, t[i]); + i = i + 1; + } + } + return n; +} + +pub fn same_text(a: []const u8, b: []const u8) -> bool { + if (a.len != b.len) { return false; } + var i : usize = 0; + while (i < a.len) { + if (a[i] != b[i]) { return false; } + i = i + 1; + } + return true; +} + +// --- Tests of the native page --------------------------------------------------------------------------------- + +pub const W_TEST_DATA : str = "{\"source\":{\"tag\":\"v1\",\"commit\":\"0123456789abcdef\"},\"fpga\":[[\"0x03636093\",\"xilinx\",\"artix a7 200t\",\"xc7a200\",6]],\"misc\":[[\"0x4ba00477\",\"ARM\",4]],\"manufacturers\":[[\"0x049\",\"Xilinx\"]],\"shadowed\":[],\"presets\":[[\"board\",\"part\",\"0x0362d093\"]],\"bench\":{\"date\":\"2026-10-04T17:28Z\",\"tool\":\"openFPGALoader v1.1.1\",\"raw\":[\"0x13636093\"],\"printed\":[\"0x03636093\"]}}"; + +fn w_has(o: []const u8, n: u32, want: []const u8) -> bool { + if (want.len > (n as usize)) { return false; } + var i : usize = 0; + while (i + want.len <= (n as usize)) { + var k : usize = 0; + while (k < want.len and o[i + k] == want[k]) { k = k + 1; } + if (k == want.len) { return true; } + i = i + 1; + } + return false; +} + +test parse_code_reads_what_tool_js_reads { + assert parse_code("0x13636093") == 325279891; + assert parse_code("0 -> 0x13636093") == 325279891; + assert parse_code("-> 0xffffffff -> 0x5") == 5; + assert parse_code("abc 0x12zz 0x34") == 52; + assert parse_code("0x123456789") == -1; + assert parse_code(" 12_34 ") == 4660; + assert parse_code("") == -1; + assert parse_code("0xffffffff") == 4294967295; + assert parse_code("zz") == -1; + assert parse_code("0x") == -1; + assert parse_code("10x12") == 18; +} + +test parse_int16_is_parseInt_16_unsigned { + assert parse_int16(" 0x1f") == 31; + assert parse_int16("-1") == 4294967295; + assert parse_int16("zz") == 0; + assert parse_int16("1ffffffff") == 4294967295; + assert parse_int16("false") == 250; + assert parse_int16("0x04014c35") == 67193909; + assert parse_int16("+0x1") == 1; +} + +test the_address_is_read_the_way_tool_js_reads_it { + assert code_from_hash("#id=0x13636093") == 325279891; + assert code_from_hash("#x=1&id=5") == 5; + assert code_from_hash("#id=") == -1; + assert code_from_hash("#id=%30x5") == 5; + assert code_from_hash("#id=%zz") == -1; + assert code_from_hash("") == -1; + assert code_from_hash("#id=&id=0x7") == 7; +} + +test the_page_opens_on_the_table_key_before_the_table_arrives { + w_init(""); + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert n > 4000; + assert w_has(o[0..], n, "

    0x03636093

    "); + assert w_has(o[0..], n, ""); + assert w_has(o[0..], n, "data-code=\"56844435\" title=\"0x03636093\" aria-pressed=\"true\">our bench, as printed"); +} + +test a_masked_version_is_found_and_said_so { + w_init(""); + w_data(0, W_TEST_DATA); + w_input(0, "0x13636093"); + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "

    In openFPGALoader v1.1.1's table after the version is masked to 0, as openFPGALoader does: key 0x03636093.

    "); + assert w_has(o[0..], n, "

    0x13636093

    "); +} + +test an_unknown_code_prints_what_jtag_cpp_prints { + w_init(""); + w_data(0, W_TEST_DATA); + w_input(0, "0x12345679"); + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "Unknown device with IDCODE: 0x12345679 (manufacturer: 0x33c (), part: 0x11 vers: 0x1"); + assert w_has(o[0..], n, "Not in openFPGALoader v1.1.1's table, with or without the version."); + assert w_has(o[0..], n, "not in openFPGALoader's manufacturer list: look up bank 7, code 0x3c in JEDEC JEP106"); +} + +test a_bit_flips_and_the_address_follows { + w_init(""); + w_data(0, W_TEST_DATA); + assert preset_count() == 3; + w_click(3); + assert CODE == 2204328083; + var h : [64]u8 = [0; 64]; + assert w_hash_out(h[0..]) == 15; + assert h[1] == 35; + assert h[14] == 51; + w_click(3 + 31); + assert CODE == 2204328082; + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "idc-bit idc-f-one idc-bad\" data-bit=\"0\""); + assert w_has(o[0..], n, "Bit 0 is 0: not an IDCODE."); +} + +test a_bad_input_says_so_and_a_good_one_clears_it { + w_init(""); + w_input(0, "zz"); + var o : [65536]u8 = [0; 65536]; + var n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "

    No IDCODE here: type up to eight hex digits, like 0x03636093.

    "); + w_input(0, " "); + n = w_render(o[0..]); + assert w_has(o[0..], n, "

    "); + w_input(0, "0x5"); + assert CODE == 5; +} + +test copy_and_save_ask_the_host_for_effects { + w_init(""); + w_click(2 + 32); + var e : [128]u8 = [0; 128]; + var n : u32 = w_effect(0, e[0..]); + assert n == 15; + assert e[0] == 117; + assert w_effect(1, e[0..]) == 0; + w_click(2 + 33); + n = w_effect(0, e[0..]); + assert e[0] == 80; + assert w_has(e[0..], n, "1200\n630\nidcode-0x03636093.png"); + assert w_effect(1, e[0..]) == 0; + w_copied(1); + var o : [65536]u8 = [0; 65536]; + const rn : u32 = w_render(o[0..]); + assert w_has(o[0..], rn, ""); + w_copied(0); + const rn2 : u32 = w_render(o[0..]); + assert w_has(o[0..], rn2, ">Copy it from the address bar"); +} + +test the_card_is_a_binary_draw_list_with_css_variables_in_it { + w_init(""); + w_data(0, W_TEST_DATA); + var o : [16384]u8 = [0; 16384]; + const n : u32 = w_bdraw2(o[0..]); + assert n > 1500; + assert o[0] == 32; + assert w_has(o[0..], n, "--w-green"); + assert w_has(o[0..], n, "--w-mono"); + assert w_has(o[0..], n, "xc7a200"); + assert w_has(o[0..], n, "t27.ai/widgets/idcode/#id=0x03636093"); +} + +test the_hash_changes_the_code_once { + w_init(""); + w_hash("#id=0x03636093"); + assert CODE == 56844435; + w_hash("#id=0x00000005"); + assert CODE == 5; + var h : [64]u8 = [0; 64]; + assert w_hash_out(h[0..]) == 15; + w_hash("#id=zz"); + assert CODE == 5; +} + +test a_table_without_a_source_leaves_the_credit_failed_and_the_table_in_use { + w_init(""); + w_data(0, "{\"fpga\":[[\"0x03636093\",\"xilinx\",\"f\",\"m\",6]],\"misc\":[],\"manufacturers\":[],\"shadowed\":[]}"); + assert TBL; + assert !P_TBL; + assert CREDIT == 2; + var o : [65536]u8 = [0; 65536]; + var n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "The device table did not load; the bit fields still work."); + assert !w_has(o[0..], n, "idc-device"); + w_input(0, "0x03636093"); + n = w_render(o[0..]); + assert w_has(o[0..], n, "
    "); +} + +/// The splicer follows only the names a module itself mentions, not the names the imported functions +/// mention. This anchor mentions every helper the writer, the JSON reader, the number rules and the +/// canvas writers call. Never called. +pub fn imports_anchor(o: []u8, s: []const u8) -> u32 { + var n : u32 = 0; + n = n + (ok(0) as u32); + n = n + OVER; + n = n + put(o, 0, 0); + n = n + raw(o, 0, s); + n = n + put_escaped(o, 0, 0); + n = n + text(o, 0, s); + n = n + num(o, 0, 0); + n = n + attr(o, 0, s, s); + n = n + fill(o, 0, s, s, s, s, 0); + n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s); + n = n + dec_to(o, 0); + n = n + (name_is(s, 0, 0, s) as u32); + n = n + (is_word_byte(0) as u32); + n = n + (H_LT as u32); + n = n + (H_GT as u32); + n = n + (H_AMP as u32); + n = n + (H_QUOTE as u32); + n = n + (H_APOS as u32); + n = n + (H_SPACE as u32); + n = n + (H_SLASH as u32); + n = n + (H_EQ as u32); + n = n + ((NO_SPAN < 0) as u32); + n = n + ((SPAN > 0) as u32); + n = n + (J_NONE as u32); + n = n + (J_OBJECT as u32); + n = n + (J_ARRAY as u32); + n = n + (J_STRING as u32); + n = n + (J_NUMBER as u32); + n = n + (J_TRUE as u32); + n = n + (J_FALSE as u32); + n = n + (J_NULL as u32); + n = n + (jspan(0, 0) as u32); + n = n + (span_lo(0) as u32); + n = n + (span_hi(0) as u32); + n = n + (skip_ws(s, 0) as u32); + n = n + (string_end(s, 0) as u32); + n = n + (value_end(s, 0) as u32); + n = n + (root(s) as u32); + n = n + (kind(s, 0) as u32); + n = n + (key_is(s, 0, 0, s) as u32); + n = n + (obj_get(s, 0, s) as u32); + n = n + arr_len(s, 0); + n = n + (arr_at(s, 0, 0) as u32); + n = n + hex4(s, 0); + n = n + escape_at(s, 0, 0); + n = n + cp_utf8_len(0); + n = n + (cp_utf8_byte(0, 0) as u32); + n = n + str_len(s, 0); + n = n + (str_byte(s, 0, 0) as u32); + n = n + JN_OVER; + n = n + JN_CAP; + n = n + ((JN_TWO52 > 0.0) as u32); + n = n + ((JN_TWO53 > 0.0) as u32); + n = n + jn_put(o, 0, 0); + n = n + jn_raw(o, 0, s); + n = n + jn_digits(o, 0); + n = n + jn_mul(o, 0, 0); + n = n + jn_mul2(o, 0, 0); + n = n + jn_mul5(o, 0, 0); + n = n + jn_shift(o, 0, 0); + n = n + (jn_cmp(s, s, 0) as u32); + n = n + (jn_expo(0.0) as u32); + n = n + (jn_mant(0.0) as u32); + n = n + ((jn_make(0, 0) > 0.0) as u32); + n = n + jn_exact(o, 0, 0); + n = n + jn_fscale(0); + n = n + (js_is_nan(0.0) as u32); + n = n + ((js_nan(0.0) > 0.0) as u32); + n = n + ((js_inf(1.0) > 0.0) as u32); + n = n + ((js_floor(0.0) > 0.0) as u32); + n = n + ((js_ceil(0.0) > 0.0) as u32); + n = n + ((js_max(0.0, 0.0) > 0.0) as u32); + n = n + ((js_min(0.0, 0.0) > 0.0) as u32); + n = n + ((js_round(0.0) > 0.0) as u32); + n = n + (jn_shortest(o, 1.0) as u32); + n = n + jn_copy(o, s, 0); + n = n + jn_put_run(o, 0, s, 0, 0); + n = n + jn_layout(o, 0, s, 0, 0); + n = n + js_str_num(o, 0, 0.0); + n = n + js_to_fixed(o, 0, 0.0, 0); + n = n + js_locale_num(o, 0, 0.0); + n = n + (jn_num_ok(s, 0, 0) as u32); + n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32); + n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32); + n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32); + n = n + (jn_abs(0) as u32); + n = n + js_str_u64(o, 0, 0); + n = n + jv_str_to(o, 0, s, 0); + n = n + jv_text_str(o, 0, s, 0); + n = n + ((jv_numval(s, 0) > 0.0) as u32); + n = n + jv_str(o, 0, s, 0); + n = n + jv_text(o, 0, s, 0); + n = n + (jv_is(s, 0, s) as u32); + n = n + (jv_truthy(s, 0) as u32); + n = n + (jv_is_ws(0) as u32); + n = n + ((jv_number_of_text(s, 0) > 0.0) as u32); + n = n + ((jv_num(s, 0) > 0.0) as u32); + n = n + jv_vput_str(o, 0, s, s); + n = n + jv_vput_f64(o, 0, s, 0.0); + n = n + jv_vput_val(o, 0, s, s, 0); + n = n + jv_vput_defined(o, 0, s, s, 0); + n = n + jv_vput_fixed(o, 0, s, 0.0, 0); + n = n + jv_kid(o, 0, s, 0); + n = n + jv_find(s, 0, s); + n = n + jv_fill(o, 0, s, s, 0, true); + n = n + (jv_string_ok(s, 0) as u32); + n = n + (jv_number_ok(s, 0) as u32); + n = n + (jv_lit_ok(s, 0, s) as u32); + n = n + (jv_valid(s) as u32); + n = n + jv_ws_at(s, 0, 0); + n = n + jv_ws_before(s, 0, 0); + n = n + jv_digit_value(0); + n = n + jv_fill_raw(o, 0, s, s, 0, true); + n = n + jv_fill_with(o, 0, s, s, 0, true, true); + n = n + num_grouped(o, 0, 0); + return n; +} diff --git a/specs/widgets/placement_map.t27 b/specs/widgets/placement_map.t27 new file mode 100644 index 0000000000..b1427fc24c --- /dev/null +++ b/specs/widgets/placement_map.t27 @@ -0,0 +1,2876 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/placement-map.t27 -- where nextpnr placed every cell of a design on the XC7A200T die +; Source of truth for public/widgets/placement-map/ (gHashTag/trinity, apps/website). The page is +; written by scripts/widget-pages-from-spec.mjs from this file and hands every constant below to +; tool.js as window.T27_WIDGET. tool.js holds no English of its own: every word on screen is a +; SAY_ constant here, every number the tool relies on is a K_ constant. +; ASCII only (L3), English only (LANG-EN). + +; WHAT IT DOES: draws the XC7A200T die as its tile columns (CLB, block RAM, DSP, IO, clock) and +; puts a dot on the site nextpnr-xilinx bound each cell to, coloured by kind: LUT, FF, CARRY4, +; BRAM, DSP, IO, BUFG. Hover or tap a dot for the cell's name, type and BEL. Drag to pan, wheel +; or pinch to zoom; "whole die" and "fit design" reset the view. Drop a routed nextpnr-xilinx JSON +; (the file --write writes) and it draws that design instead; the file is read in the browser +; with FileReader and sent nowhere. + +; DATA PROVENANCE: placement.json is written by scripts/widget-data/fpga-pnr.py from one real run +; made on 2026-10-04: trinity_v3_jtaguart.v and jtaguart.xdc from gHashTag/trinity-fpga +; fpga/openxc7-synth at de4ebda367, synthesised by yosys 0.67+post (b8e7da6f40, synth_xilinx +; -flatten -abc9 -arch xc7) and placed and routed by nextpnr-xilinx 0.9.2-107-g7037c948 on the +; xc7a200tfbg676-1 chipdb with --freq 50 --seed 1; the exact commands and sha256 of the inputs +; ship in placement.json. The bench board's XC7A200T-FGG676 has the same die and pinout +; (t27 fpga/HARDWARE_SSOT.md). The design has 373 cells: 239 LUT, 98 FF, 23 CARRY4, 5 pads with +; 5 IO buffers, 1 BUFGCTRL and nextpnr's two constant drivers; its logic sits in 60 slices. +; The die drawing (tile columns, and which tile row and column each site index lands in) comes +; from prjxray-db 517d66a383 artix7/xc7a200t/tilegrid.json; fpga-pnr.py refuses to write it if a +; site index maps to two grid lines. The slice count 33650 is the chipdb's CARRY4 BEL count and +; equals prjxray's SLICE site count, as does 8 LUT BELs per slice (269200), and both agree with +; AMD DS180 (7 Series Product Selection Guide): 33,650 slices, 134,600 six-input LUTs. + +; WHAT IT DOES NOT CLAIM: that the design was flashed or met timing on a board here; that is the +; slack-waterfall widget's question, and this is a placement only. The die drawing is the +; XC7A200T's: a dropped file from any other part is drawn on it only where its site names fall +; inside the 200T's ranges, and every cell it cannot place is listed instead of guessed at. Wires +; are not drawn. nextpnr's constant drivers sit on virtual NULL sites and are listed, not drawn. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_placement_map; + +use widgets::html_out::ok; +use widgets::html_out::OVER; +use widgets::html_out::put; +use widgets::html_out::raw; +use widgets::html_out::put_escaped; +use widgets::html_out::text; +use widgets::html_out::num; +use widgets::html_out::num_grouped; +use widgets::html_out::attr; +use widgets::html_out::fill; +use widgets::html_out::fill_named; +use widgets::html_out::dec_to; +use widgets::html_out::name_is; +use widgets::html_out::is_word_byte; +use widgets::html_out::H_LT; +use widgets::html_out::H_GT; +use widgets::html_out::H_AMP; +use widgets::html_out::H_QUOTE; +use widgets::html_out::H_APOS; +use widgets::html_out::H_SPACE; +use widgets::html_out::H_SLASH; +use widgets::html_out::H_EQ; +use widgets::json_scan::NO_SPAN; +use widgets::json_scan::SPAN; +use widgets::json_scan::J_NONE; +use widgets::json_scan::J_OBJECT; +use widgets::json_scan::J_ARRAY; +use widgets::json_scan::J_STRING; +use widgets::json_scan::J_NUMBER; +use widgets::json_scan::J_TRUE; +use widgets::json_scan::J_FALSE; +use widgets::json_scan::J_NULL; +use widgets::json_scan::jspan; +use widgets::json_scan::span_lo; +use widgets::json_scan::span_hi; +use widgets::json_scan::skip_ws; +use widgets::json_scan::string_end; +use widgets::json_scan::value_end; +use widgets::json_scan::root; +use widgets::json_scan::kind; +use widgets::json_scan::key_is; +use widgets::json_scan::obj_get; +use widgets::json_scan::arr_len; +use widgets::json_scan::arr_at; +use widgets::json_scan::hex4; +use widgets::json_scan::escape_at; +use widgets::json_scan::cp_utf8_len; +use widgets::json_scan::cp_utf8_byte; +use widgets::json_scan::str_len; +use widgets::json_scan::str_byte; +use widgets::js_num::JN_OVER; +use widgets::js_num::JN_CAP; +use widgets::js_num::JN_TWO52; +use widgets::js_num::JN_TWO53; +use widgets::js_num::jn_put; +use widgets::js_num::jn_raw; +use widgets::js_num::jn_digits; +use widgets::js_num::jn_mul; +use widgets::js_num::jn_mul2; +use widgets::js_num::jn_mul5; +use widgets::js_num::jn_shift; +use widgets::js_num::jn_cmp; +use widgets::js_num::jn_expo; +use widgets::js_num::jn_mant; +use widgets::js_num::jn_make; +use widgets::js_num::jn_exact; +use widgets::js_num::jn_fscale; +use widgets::js_num::js_is_nan; +use widgets::js_num::js_nan; +use widgets::js_num::js_inf; +use widgets::js_num::js_floor; +use widgets::js_num::js_ceil; +use widgets::js_num::js_max; +use widgets::js_num::js_min; +use widgets::js_num::js_round; +use widgets::js_num::jn_shortest; +use widgets::js_num::jn_copy; +use widgets::js_num::jn_put_run; +use widgets::js_num::jn_layout; +use widgets::js_num::js_str_num; +use widgets::js_num::js_to_fixed; +use widgets::js_num::js_locale_num; +use widgets::js_num::jn_num_ok; +use widgets::js_num::jn_cmp_text; +use widgets::js_num::js_parse_num; +use widgets::js_num::jn_dec_value; +use widgets::js_num::jn_abs; +use widgets::js_num::js_str_u64; +use widgets::js_val::jv_str_to; +use widgets::js_val::jv_text_str; +use widgets::js_val::jv_numval; +use widgets::js_val::jv_str; +use widgets::js_val::jv_text; +use widgets::js_val::jv_is; +use widgets::js_val::jv_truthy; +use widgets::js_val::jv_is_ws; +use widgets::js_val::jv_number_of_text; +use widgets::js_val::jv_num; +use widgets::js_val::jv_vput_str; +use widgets::js_val::jv_vput_f64; +use widgets::js_val::jv_vput_val; +use widgets::js_val::jv_vput_defined; +use widgets::js_val::jv_vput_fixed; +use widgets::js_val::jv_kid; +use widgets::js_val::jv_find; +use widgets::js_val::jv_fill; +use widgets::js_val::jv_string_ok; +use widgets::js_val::jv_number_ok; +use widgets::js_val::jv_lit_ok; +use widgets::js_val::jv_valid; +use widgets::js_val::jv_ws_at; +use widgets::js_val::jv_ws_before; +use widgets::js_val::jv_digit_value; +use widgets::js_val::jv_fill_raw; +use widgets::js_val::jv_fill_with; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "placement-map"; +pub const TITLE : str = "Placement map: where nextpnr put every cell on the XC7A200T"; +pub const DESCRIPTION : str = "Every LUT, FF, CARRY4, IO and clock buffer of a real nextpnr-xilinx run, drawn on its site of the Artix-7 200T die. Drop your routed JSON and it draws yours."; +pub const IMAGE_ALT : str = "A dark map of the XC7A200T die in tile columns, with a gold box around the left edge where the design sits, and the same corner enlarged beside it: green LUTs, gold flip-flops and blue carry chains in a few slices next to the IO column. Headline: 60 of 33,650 slices."; +pub const HOOK : str = "A 373-cell design on a 33,650-slice die: see exactly which sites nextpnr picked, then drop your own routed JSON."; +pub const CATEGORY : str = "fpga"; +pub const DATA_SOURCES : [5]str = ["gHashTag/trinity-fpga de4ebda367: fpga/openxc7-synth/trinity_v3_jtaguart.v and jtaguart.xdc", "yosys 0.67+post (b8e7da6f40) synth_xilinx, then nextpnr-xilinx 0.9.2-107-g7037c948 --chipdb xc7a200tfbg676-1.bin --freq 50 --seed 1 --write routed.json", "prjxray-db 517d66a383 artix7/xc7a200t/tilegrid.json: tile columns and site-to-grid maps", "scripts/widget-data/fpga-pnr.py: placement.json, commands and input sha256 inside it", "A routed nextpnr-xilinx JSON the reader drops or picks: read in the browser with FileReader, never uploaded"]; +pub const READS_LOCAL_FILES : bool = true; +pub const SENDS_NOTHING : bool = true; + +; --- Numbers the tool relies on ------------------------------------------------------------ +pub const K_DATA_FILE : str = "placement.json"; +; Cell kinds in drawing order, and the colour each is drawn in. +pub const K_KINDS : [8]str = ["LUT", "FF", "CARRY4", "BRAM", "DSP", "IO", "BUFG", "OTHER"]; +pub const K_KIND_COLORS : [8]str = ["#00ff88", "#ffd700", "#00b4ff", "#be78ff", "#ff8c3c", "#ff4d6d", "#ffffff", "#8a8a8a"]; +; A cell type is of kind K_TYPE_KIND[i] when it starts with K_TYPE_PREFIX[i]; first match wins. +pub const K_TYPE_PREFIX : [11]str = ["SLICE_LUTX", "SLICE_FFX", "CARRY4", "RAMB", "FIFO", "DSP48", "IOB", "PAD", "OBUF", "IBUF", "BUFG"]; +pub const K_TYPE_KIND : [11]str = ["LUT", "FF", "CARRY4", "BRAM", "BRAM", "DSP", "IO", "IO", "IO", "IO", "BUFG"]; +; Tile families drawn as the die, and their fill. +pub const K_TILES : [9]str = ["clbll", "clblm", "bram", "dsp", "iob", "bufg", "cmt", "gtp", "pcie"]; +pub const K_TILE_COLORS : [9]str = ["#0f1e18", "#13261e", "#281a38", "#342210", "#32141c", "#3c3c3c", "#1e1e1e", "#1a1a28", "#1a1a28"]; +; Site families the dropped file's BEL names are looked up in (placement.json geometry.maps). +pub const K_SITE_FAMILIES : [6]str = ["SLICE", "RAMB36", "RAMB18", "DSP48", "IOB", "BUFGCTRL"]; +; XC7A200T: 265 x 261 tile grid (prjxray tilegrid), 33650 slices (chipdb CARRY4 BELs, DS180). +pub const K_GRID_W : u16 = 265; +pub const K_GRID_H : u16 = 261; +pub const K_SLICES : u32 = 33650; +pub const K_LUT_BELS : u32 = 269200; +pub const K_LUT6 : u32 = 134600; +; Interaction: touch targets and the pick radius around a dot, in CSS pixels. +pub const K_MIN_TARGET_PX : u8 = 32; +pub const K_PICK_PX : u8 = 14; +pub const K_MAX_ZOOM : u8 = 80; +pub const K_MAX_FILE_MB : u16 = 512; + +; --- Words on screen ------------------------------------------------------------------------ +; Templates take {0}, {1}, ... in order. +pub const SAY_WHOLE_DIE : str = "Whole die"; +pub const SAY_FIT_DESIGN : str = "Fit design"; +pub const SAY_DEMO : str = "Demo design"; +pub const SAY_DROP : str = "Drop a routed nextpnr-xilinx JSON here, or tap to pick one. It is read in this browser and sent nowhere."; +pub const SAY_CANVAS_LABEL : str = "Map of the XC7A200T die with every placed cell as a coloured dot. Drag to pan, wheel or pinch to zoom, hover or tap a dot for its name, type and BEL."; +pub const SAY_HINT : str = "drag: pan / wheel or pinch: zoom / hover or tap: cell"; +pub const SAY_LOADING : str = "Loading the placement..."; +pub const SAY_READING : str = "Reading {0}..."; +pub const SAY_SUMMARY : str = "{0}: {1} cells placed in {2} of {3} slices"; +pub const SAY_SOURCE_DEMO : str = "{0} on {1}, placed by {2}"; +pub const SAY_SOURCE_FILE : str = "{0} ({1} bytes), {2}"; +pub const SAY_PICK_NONE : str = "Hover or tap a dot."; +pub const SAY_PICK_LINE : str = "{0} cells at this site"; +pub const SAY_FIELDS : [3]str = ["cell", "type", "BEL"]; +pub const SAY_OFF_MAP : str = "Not drawn ({0}): site outside the XC7A200T map or a virtual site"; +pub const SAY_ERR_JSON : str = "{0} is not JSON."; +pub const SAY_ERR_NO_BELS : str = "{0} has no placed cells: no NEXTPNR_BEL attributes. Use the JSON nextpnr writes with --write after routing."; +pub const SAY_ERR_SIZE : str = "{0} is larger than {1} MB."; +pub const SAY_ERR_DEMO : str = "The demo placement did not load."; +pub const SAY_COMMANDS : str = "How this was made"; + +; --- Tests ---------------------------------------------------------------------------------- +test the_die_counts_agree_with_each_other { + assert K_LUT_BELS == 8 * K_SLICES; + assert K_LUT6 == 4 * K_SLICES; + assert K_LUT6 == 134600; +} + +test every_type_prefix_has_a_kind_that_is_drawn { + assert same_text(K_TYPE_KIND[0], K_KINDS[0]); + assert same_text(K_TYPE_KIND[1], K_KINDS[1]); + assert same_text(K_TYPE_KIND[2], K_KINDS[2]); + assert same_text(K_TYPE_KIND[3], K_KINDS[3]); + assert same_text(K_TYPE_KIND[5], K_KINDS[4]); + assert same_text(K_TYPE_KIND[6], K_KINDS[5]); + assert same_text(K_TYPE_KIND[10], K_KINDS[6]); +} + +test the_touch_targets_are_big_enough { + assert K_MIN_TARGET_PX >= 32; + assert K_PICK_PX * 2 >= K_MIN_TARGET_PX - 4; + assert (K_GRID_W as u32) * (K_GRID_H as u32) == 69165; +} + +test the_page_reads_files_and_sends_nothing { + assert READS_LOCAL_FILES == true; + assert SENDS_NOTHING == true; + assert same_text(CATEGORY, "fpga"); +} + +// ===================================================================================================================== +// The native page. Everything below is written for the shared host (host.js) and replaces tool.js: the words are the +// SAY_* constants above, the numbers the K_* constants above, the state lives in the module-level variables below. +// Exports: w_init, w_view, w_measure_sel, w_canvas_keep, w_drop_root, w_draw_cap, w_file_cap, w_data, w_data_failed, +// w_click, w_cpointer, w_wheel, w_key, w_box, w_drag, w_file, w_value, w_render, w_bdraw, w_libm_need, w_libm_give. +// What the page does NOT do the way tool.js does (measured, see the report of the commit): a dropped file is read up to +// W_FILE_CAP bytes (tool.js up to K_MAX_FILE_MB); more than W_MAX_CELLS cells, a nesting deeper than 60 levels or +// modules / cells given as JSON arrays are not modelled. +// ===================================================================================================================== + +pub const W_ACCEPT : str = ".json,application/json"; +pub const W_MEASURE_SEL : str = ".pm-wrap"; +pub const W_FILE_CAP : u32 = 67108864; +pub const W_FILE_CAP_MB : u32 = 64; +pub const W_DRAW_CAP : u32 = 25165824; +pub const W_BLACK : str = "#000"; +pub const W_TILE_DEFAULT : str = "#111"; +pub const W_KIND_DEFAULT : str = "#888"; +pub const W_HOVER_COLOR : str = "#e8fff7"; +pub const W_PICK_COLOR : str = "#ffd700"; +pub const W_DASH : str = "-"; +pub const W_MAX_CELLS : u32 = 300000; +pub const W_MAX_RUNS : u32 = 65536; +pub const W_MAX_VALS : u32 = 65536; +pub const W_MAX_MODS : u32 = 4096; +pub const W_MAX_CMDS : u32 = 256; +pub const W_OFF_SHOWN : u32 = 500; +pub const W_ROWS_SHOWN : u32 = 40; + +// --- the data file (placement.json) and the geometry decoded from it --------------------------------------------------- + +pub var D_BUF : [4194304]u8 = [0; 4194304]; +pub var D_N : u32 = 0; +pub var S_GRID : bool = false; +pub var S_DEMO : bool = false; +pub var S_CELLS_OK : bool = false; +pub var G_W : f64 = 0.0; +pub var G_H : f64 = 0.0; +pub var GR_N : u32 = 0; +pub var GR_K : [65536]i32 = [0; 65536]; +pub var GR_GX : [65536]f64 = [0.0; 65536]; +pub var GR_TOP : [65536]f64 = [0.0; 65536]; +pub var GR_BOT : [65536]f64 = [0.0; 65536]; +pub var GM_N : u32 = 0; +pub var GM_NAME : [2048]u8 = [0; 2048]; +pub var GM_NAME_N : u32 = 0; +pub var GM_NO : [32]u32 = [0; 32]; +pub var GM_NL : [32]u32 = [0; 32]; +pub var GM_XO : [32]u32 = [0; 32]; +pub var GM_XN : [32]u32 = [0; 32]; +pub var GM_YO : [32]u32 = [0; 32]; +pub var GM_YN : [32]u32 = [0; 32]; +pub var GV : [65536]f64 = [0.0; 65536]; +pub var GV_N : u32 = 0; +pub var KIND_OF_PFX : [16]i32 = [0; 16]; +pub var KIND_OTHER : i32 = 0; + +// --- the cells: decoded text in one pool, the placed ones with their rectangles ------------------------------------------- + +pub var C_POOL : [33554432]u8 = [0; 33554432]; +pub var C_PN : u32 = 0; +pub var C_NO : [300000]u32 = [0; 300000]; +pub var C_NL : [300000]u32 = [0; 300000]; +pub var C_TO : [300000]u32 = [0; 300000]; +pub var C_TL : [300000]u32 = [0; 300000]; +pub var C_BO : [300000]u32 = [0; 300000]; +pub var C_BL : [300000]u32 = [0; 300000]; +pub var C_FL : [300000]u8 = [0; 300000]; +pub var C_N : u32 = 0; +pub var P_C : [300000]u32 = [0; 300000]; +pub var P_X : [300000]f64 = [0.0; 300000]; +pub var P_Y : [300000]f64 = [0.0; 300000]; +pub var P_W : [300000]f64 = [0.0; 300000]; +pub var P_H : [300000]f64 = [0.0; 300000]; +pub var P_K : [300000]u8 = [0; 300000]; +pub var P_N : u32 = 0; +pub var O_C : [512]u32 = [0; 512]; +pub var O_N : u32 = 0; +pub var K_CNT : [16]u32 = [0; 16]; +pub var SITE_H : [1048576]u32 = [0; 1048576]; +pub var S_SLICES : u32 = 0; +pub var BOX_ON : bool = false; +pub var BOX_X0 : f64 = 0.0; +pub var BOX_Y0 : f64 = 0.0; +pub var BOX_X1 : f64 = 0.0; +pub var BOX_Y1 : f64 = 0.0; + +// --- reading a dropped file: one record per cell entry, sorted as tool.js sorts ------------------------------------------- + +pub var E_KLO : [300000]u32 = [0; 300000]; +pub var E_KHI : [300000]u32 = [0; 300000]; +pub var E_RK : [300000]u32 = [0; 300000]; +pub var E_TSP : [300000]i64 = [0; 300000]; +pub var E_BSP : [300000]i64 = [0; 300000]; +pub var E_IX : [300000]u32 = [0; 300000]; +pub var E_TM : [300000]u32 = [0; 300000]; +pub var E_N : u32 = 0; +pub var MD_KLO : [4096]u32 = [0; 4096]; +pub var MD_KHI : [4096]u32 = [0; 4096]; +pub var MD_VLO : [4096]u32 = [0; 4096]; +pub var MD_VHI : [4096]u32 = [0; 4096]; +pub var MD_RK : [4096]u32 = [0; 4096]; +pub var MD_NUM : [4096]i64 = [0; 4096]; +pub var MD_N : u32 = 0; +pub var M_KLO : usize = 0; +pub var M_KHI : usize = 0; +pub var M_VLO : usize = 0; +pub var M_VHI : usize = 0; +pub var X_SCR : [262144]u8 = [0; 262144]; + +// --- the words that depend on the data ----------------------------------------------------------------------------------- + +pub var S_LABEL : [65536]u8 = [0; 65536]; +pub var S_LABEL_N : u32 = 0; +pub var S_SRC : [65536]u8 = [0; 65536]; +pub var S_SRC_N : u32 = 0; +pub var S_CMD : [262144]u8 = [0; 262144]; +pub var S_CMDO : [256]u32 = [0; 256]; +pub var S_CMDL : [256]u32 = [0; 256]; +pub var S_CMDN : u32 = 0; +pub var S_STATUS : [131072]u8 = [0; 131072]; +pub var S_STATUS_N : u32 = 0; +pub var S_FB : [262144]u8 = [0; 262144]; +pub var S_NUM : [64]u8 = [0; 64]; +pub var S_NUM3 : [64]u8 = [0; 64]; + +// --- the view, the pointers, the window ------------------------------------------------------------------------------------- + +pub var S_VX : f64 = 0.0; +pub var S_VY : f64 = 0.0; +pub var S_VS : f64 = 1.0; +pub var S_CSSW : f64 = 0.0; +pub var S_CSSH : f64 = 0.0; +pub var S_LASTW : f64 = 0.0; +pub var S_SIZED : bool = false; +pub var S_CANVAS_W : u64 = 0; +pub var S_CANVAS_H : u64 = 0; +pub var S_HOVER : i32 = 0 - 1; +pub var S_PICK : i32 = 0 - 1; +pub var S_INFO : bool = false; +pub var S_SIDE : bool = false; +pub var S_DEMO_HIDDEN : bool = true; +pub var S_OVER : bool = false; +pub var S_CLR_FILE : bool = false; +pub var ENV_W : f64 = 0.0; +pub var ENV_DPR : f64 = 1.0; +pub var ENV_EMBED : u32 = 0; +pub var ENV_IH : f64 = 768.0; +pub var ENV_BW : f64 = 0.0; +pub var PT_ID : [8]i64 = [0; 8]; +pub var PT_X : [8]f64 = [0.0; 8]; +pub var PT_Y : [8]f64 = [0.0; 8]; +pub var PT_N : u32 = 0; +pub var DR_ON : bool = false; +pub var DR_SX : f64 = 0.0; +pub var DR_SY : f64 = 0.0; +pub var DR_VX : f64 = 0.0; +pub var DR_VY : f64 = 0.0; +pub var DR_VS : f64 = 0.0; +pub var DR_MOVED : bool = false; +pub var PI_ON : bool = false; +pub var PI_D : f64 = 0.0; +pub var PI_S : f64 = 0.0; +pub var LB_OP : [64]u8 = [0; 64]; +pub var LB_A : [64]f64 = [0.0; 64]; +pub var LB_R : [64]f64 = [0.0; 64]; +pub var LB_N : u32 = 0; +pub var RQ_OP : [64]u8 = [0; 64]; +pub var RQ_A : [64]f64 = [0.0; 64]; +pub var RQ_N : u32 = 0; +pub var RQ_SENT : u32 = 0; +pub var MISSING : bool = false; + +// --- small helpers ----------------------------------------------------------------------------------------------------------- + +pub fn same_text(a: []const u8, b: []const u8) -> bool { + if (a.len != b.len) { return false; } + var i : usize = 0; + while (i < a.len) { + if (a[i] != b[i]) { return false; } + i = i + 1; + } + return true; +} + +fn copy_into(dst: []u8, at: u32, s: []const u8) -> u32 { + var n : u32 = at; + var k : usize = 0; + while (k < s.len and (n as usize) < dst.len) { + dst[n as usize] = s[k]; + n = n + 1; + k = k + 1; + } + return n; +} + +fn is_digit(b: u8) -> bool { + return b >= 48 and b <= 57; +} + +/// Does the text start with the prefix? +fn starts(t: []const u8, w: []const u8) -> bool { + if (t.len < w.len) { return false; } + var k : usize = 0; + while (k < w.len) { + if (t[k] != w[k]) { return false; } + k = k + 1; + } + return true; +} + +// --- fmt(): the templates {0} {1} ... of the words, with the arguments in ARB ----------------------------------------------------- + +pub var ARB : [262144]u8 = [0; 262144]; +pub var ARO : [8]u32 = [0; 8]; +pub var ARL : [8]u32 = [0; 8]; + +pub fn fmt_into(dst: []u8, tpl: []const u8, nargs: u32) -> i64 { + var n : usize = 0; + var k : usize = 0; + while (k < tpl.len) { + var done : bool = false; + if (tpl[k] == 123) { + var q : usize = k + 1; + var idx : u64 = 0; + while (q < tpl.len and is_digit(tpl[q])) { + if (idx < 100000) { idx = idx * 10 + ((tpl[q] - 48) as u64); } + q = q + 1; + } + if (q > k + 1 and q < tpl.len and tpl[q] == 125) { + if (idx < (nargs as u64)) { + var a : usize = 0; + while (a < (ARL[idx as usize] as usize)) { + if (n >= dst.len) { return 0 - 1; } + dst[n] = ARB[(ARO[idx as usize] as usize) + a]; + n = n + 1; + a = a + 1; + } + } + k = q + 1; + done = true; + } + } + if (!done) { + if (n >= dst.len) { return 0 - 1; } + dst[n] = tpl[k]; + n = n + 1; + k = k + 1; + } + } + return n as i64; +} + +fn arg_at(i: usize) -> usize { + if (i == 0) { return 0; } + return (ARO[i - 1] + ARL[i - 1]) as usize; +} + +pub fn arg_set(i: usize, w: []const u8, lo: usize, hi: usize) -> bool { + const at : usize = arg_at(i); + if (at + (hi - lo) > 262144) { return false; } + ARO[i] = at as u32; + ARL[i] = (hi - lo) as u32; + var k : usize = lo; + while (k < hi) { + ARB[at + (k - lo)] = w[k]; + k = k + 1; + } + return true; +} + +fn arg_text(i: usize, s: []const u8) -> bool { + return arg_set(i, s, 0, s.len); +} + +/// arg i = num(x), Number.toLocaleString('en-US') +fn arg_loc(i: usize, x: f64) -> bool { + const e : u32 = js_locale_num(S_NUM[0..], 0, x); + if (e == OVER) { return arg_set(i, S_NUM[0..], 0, 0); } + return arg_set(i, S_NUM[0..], 0, e as usize); +} + +/// arg i = String(x) for a number +fn arg_num(i: usize, x: f64) -> bool { + const at : usize = arg_at(i); + if (at + 400 > 262144) { return false; } + ARO[i] = at as u32; + const e : u32 = js_str_num(ARB[0 .. 262144], at as u32, x); + if (e == OVER) { return false; } + ARL[i] = e - (at as u32); + return true; +} + +/// arg i = String(v) of the JSON value at sp; with `nullish` a null or missing value is '' (the `v ?? ''` of fmt) +fn arg_val(i: usize, d: []const u8, sp: i64, nullish: bool) -> bool { + const at : usize = arg_at(i); + ARO[i] = at as u32; + ARL[i] = 0; + const k : u8 = kind(d, sp); + if (nullish and (k == J_NONE or k == J_NULL)) { return true; } + if (at + 400 > 262144) { return false; } + const e : u32 = jv_str(ARB[0 .. 262144], at as u32, d, sp); + if (e == OVER) { return false; } + ARL[i] = e - (at as u32); + return true; +} + +fn status_is(s: []const u8) -> u32 { + S_STATUS_N = copy_into(S_STATUS[0..], 0, s); + return 0; +} + +fn status_fmt(tpl: []const u8, nargs: u32) -> u32 { + const l : i64 = fmt_into(S_STATUS[0..], tpl, nargs); + S_STATUS_N = 0; + if (l >= 0) { S_STATUS_N = l as u32; } + return 0; +} + +/// A DOM unsigned long from a double: truncated, modulo 2^32. +fn to_uint32(x: f64) -> u64 { + if (x != x or x - x != 0.0) { return 0; } + var t : f64 = js_floor(x); + if (x < 0.0) { t = js_ceil(x); } + const m : f64 = t - 4294967296.0 * js_floor(t / 4294967296.0); + return m as u64; +} + +/// Math.hypot(a, b), V8's algorithm: the larger argument scales, a Kahan sum of the squares. +pub fn hypot2(a0: f64, b0: f64) -> f64 { + var a : f64 = a0; + var b : f64 = b0; + if (a != a or b != b) { + if (a - a != 0.0 and a == a) { return js_inf(0.0); } + if (b - b != 0.0 and b == b) { return js_inf(0.0); } + return js_nan(0.0); + } + if (a < 0.0) { a = 0.0 - a; } + if (b < 0.0) { b = 0.0 - b; } + var mx : f64 = 0.0; + if (a > mx) { mx = a; } + if (b > mx) { mx = b; } + if (mx == js_inf(0.0)) { return mx; } + if (mx == 0.0) { return 0.0; } + var sum : f64 = 0.0; + var comp : f64 = 0.0; + var k : u32 = 0; + while (k < 2) { + var v : f64 = a; + if (k == 1) { v = b; } + const n : f64 = v / mx; + const summand : f64 = n * n - comp; + const prelim : f64 = sum + summand; + comp = (prelim - sum) - summand; + sum = prelim; + k = k + 1; + } + return @sqrt(sum) * mx; +} + +/// Math.abs +fn abs_f(x: f64) -> f64 { + if (x < 0.0) { return 0.0 - x; } + return x; +} + +// --- boot ------------------------------------------------------------------------------------------------------------------------ + +fn kind_index(k: []const u8) -> i32 { + var i : usize = 0; + while (i < K_KINDS.len) { + if (same_text(K_KINDS[i], k)) { return i as i32; } + i = i + 1; + } + return 0 - 1; +} + +fn init_kinds() -> u32 { + var i : usize = 0; + while (i < K_TYPE_PREFIX.len and i < 16) { + KIND_OF_PFX[i] = kind_index(K_TYPE_KIND[i]); + i = i + 1; + } + KIND_OTHER = kind_index("OTHER"); + return 0; +} + +pub fn w_init(h: []const u8) -> u32 { + init_kinds(); + S_GRID = false; + S_DEMO = false; + S_CELLS_OK = false; + GR_N = 0; + GM_N = 0; + GV_N = 0; + C_N = 0; + C_PN = 0; + P_N = 0; + O_N = 0; + S_SLICES = 0; + BOX_ON = false; + S_LABEL_N = 0; + S_SRC_N = 0; + S_CMDN = 0; + S_VX = 0.0; + S_VY = 0.0; + S_VS = 1.0; + S_CSSW = 0.0; + S_CSSH = 0.0; + S_LASTW = 0.0; + S_SIZED = false; + S_HOVER = 0 - 1; + S_PICK = 0 - 1; + S_INFO = false; + S_SIDE = false; + S_DEMO_HIDDEN = true; + S_OVER = false; + S_CLR_FILE = false; + PT_N = 0; + DR_ON = false; + PI_ON = false; + LB_N = 0; + RQ_N = 0; + MISSING = false; + var q : usize = 0; + while (q < 16) { + K_CNT[q] = 0; + q = q + 1; + } + status_is(SAY_LOADING); + return 0; +} + +pub fn w_view(width: f64, dpr: f64, embed: u32, ih: f64, bw: f64) -> u32 { + ENV_W = width; + ENV_DPR = dpr; + ENV_EMBED = embed; + ENV_IH = ih; + ENV_BW = bw; + return 0; +} + +pub fn w_measure_sel(dst: []u8) -> u32 { + return copy_into(dst, 0, W_MEASURE_SEL); +} + +pub fn w_canvas_keep() -> u32 { + return 1; +} + +pub fn w_drop_root() -> u32 { + return 1; +} + +pub fn w_draw_cap() -> u32 { + return W_DRAW_CAP; +} + +pub fn w_file_cap() -> u32 { + return W_FILE_CAP; +} + +// --- JSON members and elements, walked in one pass ----------------------------------------------------------------------------- + +/// The member of the object ending at hi that starts at or after p0: sets M_KLO/M_KHI (the key with its quotes) and +/// M_VLO/M_VHI (the value), returns the position after it, or -1 when there is no member there. +fn mem_next(d: []const u8, hi: usize, p0: usize) -> i64 { + const p : usize = skip_ws(d, p0); + if (p >= hi or d[p] != 34) { return 0 - 1; } + const ke : usize = string_end(d, p); + if (ke > hi) { return 0 - 1; } + const q : usize = skip_ws(d, ke); + if (q >= hi or d[q] != 58) { return 0 - 1; } + const v : usize = skip_ws(d, q + 1); + const ve : usize = value_end(d, v); + if (ve > hi) { return 0 - 1; } + M_KLO = p; + M_KHI = ke; + M_VLO = v; + M_VHI = ve; + var nx : usize = skip_ws(d, ve); + if (nx < hi and d[nx] == 44) { nx = skip_ws(d, nx + 1); } + return nx as i64; +} + +/// The element of the array ending at hi that starts at or after p0: sets M_VLO/M_VHI, returns the position after it, or -1. +fn el_next(d: []const u8, hi: usize, p0: usize) -> i64 { + const p : usize = skip_ws(d, p0); + if (p + 1 >= hi) { return 0 - 1; } + const ve : usize = value_end(d, p); + if (ve > hi) { return 0 - 1; } + M_VLO = p; + M_VHI = ve; + var nx : usize = skip_ws(d, ve); + if (nx < hi and d[nx] == 44) { nx = skip_ws(d, nx + 1); } + return nx as i64; +} + +/// Is the key span (quotes included) the key, escapes decoded? A plain key is compared byte for byte. +fn key_eq(d: []const u8, lo: usize, hi: usize, key: []const u8) -> bool { + if (hi < lo + 2) { return false; } + var plain : bool = true; + var p : usize = lo + 1; + while (p + 1 < hi) { + if (d[p] == 92) { plain = false; } + p = p + 1; + } + if (plain) { + if (hi - lo - 2 != key.len) { return false; } + var k : usize = 0; + while (k < key.len) { + if (d[lo + 1 + k] != key[k]) { return false; } + k = k + 1; + } + return true; + } + return key_is(d, lo, hi, key); +} + +/// The value of `key` in the object at sp; the LAST such key, as JSON.parse keeps it. NO_SPAN when absent. +fn obj_last(d: []const u8, sp: i64, key: []const u8) -> i64 { + if (kind(d, sp) != J_OBJECT) { return NO_SPAN; } + const hi : usize = span_hi(sp); + var res : i64 = NO_SPAN; + var p : usize = span_lo(sp) + 1; + var go : bool = true; + while (go) { + const nx : i64 = mem_next(d, hi, p); + if (nx < 0) { + go = false; + } else { + if (key_eq(d, M_KLO, M_KHI, key)) { res = jspan(M_VLO, M_VHI); } + p = nx as usize; + } + } + return res; +} + +/// The i-th element of the array at sp (arr_at is quadratic when called in a loop; this is for a few elements). +fn el_at(d: []const u8, sp: i64, i: u32) -> i64 { + return arr_at(d, sp, i); +} + +/// The code point at p of a JSON string body (escapes decoded, UTF-8 read): cp * 16 + the bytes it takes. +fn cp_at(d: []const u8, p: usize, hi: usize) -> u32 { + const b : u8 = d[p]; + if (b == 92 and p + 1 < hi) { + const e : u32 = escape_at(d, p + 1, hi); + return (e / 16) * 16 + 1 + (e % 16); + } + if (b < 128) { return (b as u32) * 16 + 1; } + if (b >= 240 and p + 3 < hi) { + const c4 : u32 = ((b - 240) as u32) * 262144 + ((d[p + 1] % 64) as u32) * 4096 + ((d[p + 2] % 64) as u32) * 64 + ((d[p + 3] % 64) as u32); + return c4 * 16 + 4; + } + if (b >= 224 and b < 240 and p + 2 < hi) { + const c3 : u32 = ((b - 224) as u32) * 4096 + ((d[p + 1] % 64) as u32) * 64 + ((d[p + 2] % 64) as u32); + return c3 * 16 + 3; + } + if (b >= 192 and b < 224 and p + 1 < hi) { + const c2 : u32 = ((b - 192) as u32) * 64 + ((d[p + 1] % 64) as u32); + return c2 * 16 + 2; + } + return (b as u32) * 16 + 1; +} + +/// The first UTF-16 unit of a code point. +fn unit_of(cp: u32) -> u32 { + if (cp < 65536) { return cp; } + return 55296 + (cp - 65536) / 1024; +} + +/// a < b as JavaScript compares two strings (UTF-16 code units, a prefix first): -1, 0 or 1, for the two key spans. +fn key_cmp(d: []const u8, alo: usize, ahi: usize, blo: usize, bhi: usize) -> i32 { + var pa : usize = alo + 1; + var pb : usize = blo + 1; + const ea : usize = ahi - 1; + const eb : usize = bhi - 1; + while (true) { + if (pa >= ea and pb >= eb) { return 0; } + if (pa >= ea) { return 0 - 1; } + if (pb >= eb) { return 1; } + const ca : u32 = cp_at(d, pa, ea); + const cb : u32 = cp_at(d, pb, eb); + const xa : u32 = ca / 16; + const xb : u32 = cb / 16; + if (xa != xb) { + const ua : u32 = unit_of(xa); + const ub : u32 = unit_of(xb); + if (ua != ub) { + if (ua < ub) { return 0 - 1; } + return 1; + } + if (xa < xb) { return 0 - 1; } + return 1; + } + pa = pa + ((ca % 16) as usize); + pb = pb + ((cb % 16) as usize); + } + return 0; +} + +// --- the demo data file: geometry, source lines, cells --------------------------------------------------------------------------- + +/// A JSON number as a double, NaN when the value is no number. +fn jnum(d: []const u8, sp: i64) -> f64 { + if (kind(d, sp) != J_NUMBER) { return js_nan(0.0); } + return jv_numval(d, sp); +} + +/// The index of the tile family named by the JSON string at sp in K_TILES, -1 when it is none of them. +fn tile_index(d: []const u8, sp: i64) -> i32 { + var i : usize = 0; + while (i < K_TILES.len) { + if (jv_is(d, sp, K_TILES[i])) { return i as i32; } + i = i + 1; + } + return 0 - 1; +} + +fn gm_find(d: []const u8, sp: i64) -> i32 { + var i : u32 = 0; + while (i < GM_N) { + const o : usize = GM_NO[i as usize] as usize; + const l : usize = GM_NL[i as usize] as usize; + if (str_len(d, sp) == (l as u32)) { + var k : usize = 0; + var same : bool = true; + while (k < l) { + if (GM_NAME[o + k] != str_byte(d, sp, k as u32)) { same = false; } + k = k + 1; + } + if (same) { return i as i32; } + } + i = i + 1; + } + return 0 - 1; +} + +/// The numbers of the JSON array at sp into GV; the offset, or -1 when something is no number or the pool is full. +fn gv_numbers(d: []const u8, sp: i64) -> i64 { + if (kind(d, sp) != J_ARRAY) { return 0 - 1; } + const start : u32 = GV_N; + const hi : usize = span_hi(sp); + var p : usize = span_lo(sp) + 1; + var go : bool = true; + while (go) { + const nx : i64 = el_next(d, hi, p); + if (nx < 0) { + go = false; + } else { + if (GV_N >= W_MAX_VALS) { return 0 - 1; } + const v : f64 = jnum(d, jspan(M_VLO, M_VHI)); + if (v != v) { return 0 - 1; } + GV[GV_N as usize] = v; + GV_N = GV_N + 1; + p = nx as usize; + } + } + return start as i64; +} + +/// geometry of the demo: grid size, the runs of tiles, the site maps of each family. +fn decode_geometry(d: []const u8, geo: i64) -> bool { + G_W = jnum(d, obj_last(d, geo, "grid_w")); + G_H = jnum(d, obj_last(d, geo, "grid_h")); + if (G_W != G_W or G_H != G_H) { return false; } + const runs : i64 = obj_last(d, geo, "runs"); + if (kind(d, runs) != J_ARRAY) { return false; } + GR_N = 0; + var p : usize = span_lo(runs) + 1; + const hi : usize = span_hi(runs); + var go : bool = true; + while (go) { + const nx : i64 = el_next(d, hi, p); + if (nx < 0) { + go = false; + } else { + const e : i64 = jspan(M_VLO, M_VHI); + if (kind(d, e) != J_ARRAY or GR_N >= W_MAX_RUNS) { return false; } + const gx : f64 = jnum(d, el_at(d, e, 1)); + const top : f64 = jnum(d, el_at(d, e, 2)); + const bot : f64 = jnum(d, el_at(d, e, 3)); + if (gx != gx or top != top or bot != bot) { return false; } + GR_K[GR_N as usize] = tile_index(d, el_at(d, e, 0)); + GR_GX[GR_N as usize] = gx; + GR_TOP[GR_N as usize] = top; + GR_BOT[GR_N as usize] = bot; + GR_N = GR_N + 1; + p = nx as usize; + } + } + const maps : i64 = obj_last(d, geo, "maps"); + // geom.maps[fam]: absent or null throws at the first cell; any other value that is no object has no families + if (kind(d, maps) == J_NONE or kind(d, maps) == J_NULL) { return false; } + GM_N = 0; + GM_NAME_N = 0; + GV_N = 0; + const mh : usize = span_hi(maps); + var q : usize = span_lo(maps) + 1; + go = kind(d, maps) == J_OBJECT; + while (go) { + const mx : i64 = mem_next(d, mh, q); + if (mx < 0) { + go = false; + } else { + const ksp : i64 = jspan(M_KLO, M_KHI); + const fam : i64 = jspan(M_VLO, M_VHI); + if (kind(d, fam) != J_OBJECT) { return false; } + const xs : i64 = gv_numbers(d, obj_last(d, fam, "x")); + const ys : i64 = gv_numbers(d, obj_last(d, fam, "y")); + if (xs < 0 or ys < 0) { return false; } + var slot : i32 = gm_find(d, ksp); + if (slot < 0) { + if (GM_N >= 32) { return false; } + slot = GM_N as i32; + GM_NO[GM_N as usize] = GM_NAME_N; + const e2 : u32 = jv_str_to(GM_NAME[0..], GM_NAME_N, d, ksp); + if (e2 == OVER) { return false; } + GM_NL[GM_N as usize] = e2 - GM_NAME_N; + GM_NAME_N = e2; + GM_N = GM_N + 1; + } + const s : usize = slot as usize; + GM_XO[s] = xs as u32; + GM_XN[s] = arr_len(d, obj_last(d, fam, "x")); + GM_YO[s] = ys as u32; + GM_YN[s] = arr_len(d, obj_last(d, fam, "y")); + q = mx as usize; + } + } + return true; +} + +/// state.label, state.sourceLine, state.commands of the demo (src = demo.source || {}). +fn demo_words(d: []const u8, r: i64) -> bool { + const src : i64 = obj_last(d, r, "source"); + const top : i64 = obj_last(d, src, "top"); + S_LABEL_N = 0; + if (jv_truthy(d, top)) { + const e : u32 = jv_str(S_LABEL[0..], 0, d, top); + if (e == OVER) { return false; } + S_LABEL_N = e; + } + arg_val(0, d, obj_last(d, src, "design"), true); + arg_val(1, d, obj_last(d, src, "part"), true); + arg_val(2, d, obj_last(d, src, "nextpnr"), true); + const sl : i64 = fmt_into(S_SRC[0..], SAY_SOURCE_DEMO, 3); + S_SRC_N = 0; + if (sl >= 0) { S_SRC_N = sl as u32; } + S_CMDN = 0; + const cm : i64 = obj_last(d, src, "commands"); + if (jv_truthy(d, cm)) { + if (kind(d, cm) != J_ARRAY) { return false; } + var at : u32 = 0; + var p : usize = span_lo(cm) + 1; + const hi : usize = span_hi(cm); + var go : bool = true; + while (go) { + const nx : i64 = el_next(d, hi, p); + if (nx < 0) { + go = false; + } else { + if (S_CMDN >= W_MAX_CMDS) { return false; } + const sp : i64 = jspan(M_VLO, M_VHI); + const k : u8 = kind(d, sp); + S_CMDO[S_CMDN as usize] = at; + var e : u32 = at; + if (k != J_NULL and k != J_FALSE) { e = jv_str(S_CMD[0..], at, d, sp); } + if (e == OVER) { return false; } + S_CMDL[S_CMDN as usize] = e - at; + at = e; + S_CMDN = S_CMDN + 1; + p = nx as usize; + } + } + } + return true; +} + +/// One cell's three texts into the pool: the String() of the JSON value at sp. Returns false when the pool is full. +fn pool_val(d: []const u8, sp: i64, which: u32, ci: u32) -> bool { + const e : u32 = jv_str(C_POOL[0..], C_PN, d, sp); + if (e == OVER) { return false; } + const o : u32 = C_PN; + const l : u32 = e - C_PN; + C_PN = e; + if (which == 0) { C_NO[ci as usize] = o; C_NL[ci as usize] = l; } + if (which == 1) { C_TO[ci as usize] = o; C_TL[ci as usize] = l; } + if (which == 2) { C_BO[ci as usize] = o; C_BL[ci as usize] = l; } + return true; +} + +/// demo.cells into the pool, each [name, type, bel]; false when it is no array of arrays or the pool is full. +fn demo_cells(d: []const u8, cells: i64) -> bool { + C_N = 0; + C_PN = 0; + if (kind(d, cells) != J_ARRAY) { return false; } + const hi : usize = span_hi(cells); + var p : usize = span_lo(cells) + 1; + var go : bool = true; + while (go) { + const nx : i64 = el_next(d, hi, p); + if (nx < 0) { + go = false; + } else { + const e : i64 = jspan(M_VLO, M_VHI); + if (kind(d, e) != J_ARRAY or C_N >= W_MAX_CELLS) { return false; } + const a : i64 = arr_at(d, e, 0); + const b : i64 = arr_at(d, e, 1); + const c : i64 = arr_at(d, e, 2); + const ci : u32 = C_N; + if (!pool_val(d, a, 0, ci) or !pool_val(d, b, 1, ci) or !pool_val(d, c, 2, ci)) { return false; } + var fl : u8 = 0; + const ka : u8 = kind(d, a); + if (ka == J_NONE or ka == J_NULL or ka == J_FALSE) { fl = fl + 1; } + const kb : u8 = kind(d, b); + if (kb == J_NONE or kb == J_NULL or kb == J_FALSE) { fl = fl + 2; } + if (!jv_truthy(d, c)) { fl = fl + 4; } + C_FL[ci as usize] = fl; + C_N = C_N + 1; + p = nx as usize; + } + } + return true; +} + +// --- placing a cell on the die --------------------------------------------------------------------------------------------- + +fn kind_of_cell(ci: u32) -> i32 { + const t : []const u8 = C_POOL[(C_TO[ci as usize] as usize)..((C_TO[ci as usize] + C_TL[ci as usize]) as usize)]; + var i : usize = 0; + while (i < K_TYPE_PREFIX.len and i < 16) { + if (starts(t, K_TYPE_PREFIX[i])) { return KIND_OF_PFX[i]; } + i = i + 1; + } + return KIND_OTHER; +} + +fn type_starts(ci: u32, w: []const u8) -> bool { + return starts(C_POOL[(C_TO[ci as usize] as usize)..((C_TO[ci as usize] + C_TL[ci as usize]) as usize)], w); +} + +/// The length of the site of a cell: its BEL up to the first slash. +fn site_len(ci: u32) -> u32 { + const o : usize = C_BO[ci as usize] as usize; + const l : u32 = C_BL[ci as usize]; + var k : u32 = 0; + while (k < l and C_POOL[o + (k as usize)] != 47) { k = k + 1; } + return k; +} + +fn site_same(a: u32, b: u32) -> bool { + const la : u32 = site_len(a); + if (la != site_len(b)) { return false; } + const oa : usize = C_BO[a as usize] as usize; + const ob : usize = C_BO[b as usize] as usize; + var k : usize = 0; + while (k < (la as usize)) { + if (C_POOL[oa + k] != C_POOL[ob + k]) { return false; } + k = k + 1; + } + return true; +} + +fn fam_is(ci: u32, flen: u32, w: []const u8) -> bool { + if ((flen as usize) != w.len) { return false; } + const o : usize = C_BO[ci as usize] as usize; + var k : usize = 0; + while (k < w.len) { + if (C_POOL[o + k] != w[k]) { return false; } + k = k + 1; + } + return true; +} + +/// place(cell, geom): the rectangle of the cell in grid units, appended to P_*; false when it is off the map. +fn place_cell(ci: u32) -> bool { + const o : usize = C_BO[ci as usize] as usize; + const sl : u32 = site_len(ci); + const bl : u32 = C_BL[ci as usize]; + // ^([A-Z0-9]+)_X(\d+)Y(\d+)$ + var i : u32 = 0; + while (i < sl and ((C_POOL[o + (i as usize)] >= 65 and C_POOL[o + (i as usize)] <= 90) or is_digit(C_POOL[o + (i as usize)]))) { i = i + 1; } + const flen : u32 = i; + if (flen == 0) { return false; } + if (i + 1 >= sl or C_POOL[o + (i as usize)] != 95 or C_POOL[o + (i as usize) + 1] != 88) { return false; } + i = i + 2; + var sx : f64 = 0.0; + var nd : u32 = 0; + while (i < sl and is_digit(C_POOL[o + (i as usize)])) { + sx = sx * 10.0 + ((C_POOL[o + (i as usize)] - 48) as f64); + i = i + 1; + nd = nd + 1; + } + if (nd == 0) { return false; } + if (i >= sl or C_POOL[o + (i as usize)] != 89) { return false; } + i = i + 1; + var sy : f64 = 0.0; + nd = 0; + while (i < sl and is_digit(C_POOL[o + (i as usize)])) { + sy = sy * 10.0 + ((C_POOL[o + (i as usize)] - 48) as f64); + i = i + 1; + nd = nd + 1; + } + if (nd == 0 or i != sl) { return false; } + // FIFO18 is drawn with the RAMB18 map + var fname : []const u8 = C_POOL[o..(o + (flen as usize))]; + if (fam_is(ci, flen, "FIFO18")) { fname = "RAMB18"; } + var g : i32 = 0 - 1; + var m : u32 = 0; + while (m < GM_N) { + if (same_text(GM_NAME[(GM_NO[m as usize] as usize)..((GM_NO[m as usize] + GM_NL[m as usize]) as usize)], fname)) { g = m as i32; } + m = m + 1; + } + if (g < 0) { return false; } + const gi : usize = g as usize; + if (sx >= (GM_XN[gi] as f64) or sy >= (GM_YN[gi] as f64)) { return false; } + const gxv : f64 = GV[(GM_XO[gi] as usize) + (sx as usize)]; + const gyv : f64 = GV[(GM_YO[gi] as usize) + (sy as usize)]; + if (gxv < 0.0 or gyv < 0.0) { return false; } + const n : usize = P_N as usize; + P_C[n] = ci; + P_K[n] = kind_of_cell(ci) as u8; + if (same_text(fname, "SLICE")) { + var half : f64 = 0.0; + if (((sx as u64) % 2) == 1) { half = 0.46; } + var letter : f64 = 0.0; + if (sl + 1 < bl) { + const ch : u8 = C_POOL[o + (sl as usize) + 1]; + if (ch >= 65 and ch <= 68) { letter = (ch - 65) as f64; } + } + var col : f64 = 0.0; + if (type_starts(ci, "SLICE_FFX")) { col = 1.0; } else { + if (type_starts(ci, "CARRY4")) { col = 2.0; } + } + var yoff : f64 = letter * 0.25; + var hh : f64 = 0.25; + if (type_starts(ci, "CARRY4")) { + yoff = 0.0; + hh = 1.0; + } + P_X[n] = gxv + half + col * 0.15; + P_Y[n] = gyv + yoff; + P_W[n] = 0.14; + P_H[n] = hh; + return true; + } + P_X[n] = gxv; + P_Y[n] = gyv; + P_W[n] = 1.5; + P_H[n] = 1.5; + if (same_text(fname, "RAMB36") or same_text(fname, "DSP48")) { + P_Y[n] = gyv - 4.0; + P_W[n] = 0.9; + P_H[n] = 5.0; + return true; + } + if (same_text(fname, "RAMB18")) { + P_Y[n] = gyv - 2.0; + P_W[n] = 0.9; + P_H[n] = 2.5; + return true; + } + if (same_text(fname, "IOB")) { + P_W[n] = 0.9; + P_H[n] = 1.0; + return true; + } + return true; +} + +/// The hash set of the distinct sites of the slice and carry cells: true when the site of ci is new. +fn site_add(ci: u32) -> bool { + var h : u64 = 7; + const o : usize = C_BO[ci as usize] as usize; + const sl : u32 = site_len(ci); + var k : usize = 0; + while (k < (sl as usize)) { + h = (h * 31 + (C_POOL[o + k] as u64)) % 4294967296; + k = k + 1; + } + var slot : u64 = h % 1048576; + var tries : u32 = 0; + while (tries < 1048576) { + const v : u32 = SITE_H[slot as usize]; + if (v == 0) { + SITE_H[slot as usize] = ci + 1; + return true; + } + if (site_same(v - 1, ci)) { return false; } + slot = (slot + 1) % 1048576; + tries = tries + 1; + } + return false; +} + +fn is_slice_cell(ci: u32) -> bool { + return type_starts(ci, "SLICE_") or type_starts(ci, "CARRY4"); +} + +// --- load(cells, geom) -------------------------------------------------------------------------------------------------------- + +/// The rest of load() once the cells are in the pool: kinds, placing, the slice count, the box; then renderSide and fit(true). +fn finish_load() -> u32 { + P_N = 0; + O_N = 0; + S_SLICES = 0; + var q : usize = 0; + while (q < 16) { + K_CNT[q] = 0; + q = q + 1; + } + var z : u32 = 0; + while (z < 1048576) { + SITE_H[z as usize] = 0; + z = z + 1; + } + var ci : u32 = 0; + while (ci < C_N) { + const k : i32 = kind_of_cell(ci); + if (k >= 0 and k < 16) { K_CNT[k as usize] = K_CNT[k as usize] + 1; } + if (place_cell(ci)) { + P_N = P_N + 1; + } else { + if (O_N < W_OFF_SHOWN) { O_C[O_N as usize] = ci; } + O_N = O_N + 1; + } + if (is_slice_cell(ci)) { + if (site_add(ci)) { S_SLICES = S_SLICES + 1; } + } + ci = ci + 1; + } + // the box around the slices (else around every placed cell) + var any : bool = false; + var i : u32 = 0; + while (i < P_N) { + if (is_slice_cell(P_C[i as usize])) { any = true; } + i = i + 1; + } + BOX_ON = false; + var x0 : f64 = js_inf(0.0); + var y0 : f64 = js_inf(0.0); + var x1 : f64 = 0.0 - js_inf(0.0); + var y1 : f64 = 0.0 - js_inf(0.0); + i = 0; + while (i < P_N) { + if (!any or is_slice_cell(P_C[i as usize])) { + BOX_ON = true; + x0 = js_min(x0, P_X[i as usize]); + y0 = js_min(y0, P_Y[i as usize]); + x1 = js_max(x1, P_X[i as usize] + P_W[i as usize]); + y1 = js_max(y1, P_Y[i as usize] + P_H[i as usize]); + } + i = i + 1; + } + if (BOX_ON) { + BOX_X0 = x0 - 4.0; + BOX_Y0 = y0 - 4.0; + BOX_X1 = x1 + 4.0; + BOX_Y1 = y1 + 4.0; + } + S_PICK = 0 - 1; + S_HOVER = 0 - 1; + // renderSide(): the summary line + S_SIDE = true; + S_INFO = true; + arg_set(0, S_LABEL[0..], 0, S_LABEL_N as usize); + arg_loc(1, C_N as f64); + arg_loc(2, S_SLICES as f64); + arg_loc(3, K_SLICES as f64); + status_fmt(SAY_SUMMARY, 4); + fit(true); + return 0; +} + +/// showDemo(): the demo's words and cells. +fn show_demo() -> u32 { + if (!S_DEMO) { return 0; } + const d : []const u8 = D_BUF[0..(D_N as usize)]; + const r : i64 = root(d); + if (!demo_words(d, r)) { + status_is(SAY_ERR_DEMO); + return 0; + } + S_DEMO_HIDDEN = true; + if (!demo_cells(d, obj_last(d, r, "cells"))) { + // tool.js: the loop over cells throws after placed and offMap were emptied + P_N = 0; + O_N = 0; + status_is(SAY_ERR_DEMO); + return 0; + } + return finish_load(); +} + +fn data_ready() -> u32 { + const d : []const u8 = D_BUF[0..(D_N as usize)]; + if (!jv_valid(d)) { + status_is(SAY_ERR_DEMO); + return 0; + } + const r : i64 = root(d); + const geo : i64 = obj_last(d, r, "geometry"); + if (kind(d, r) != J_OBJECT or kind(d, geo) != J_OBJECT or !decode_geometry(d, geo)) { + status_is(SAY_ERR_DEMO); + return 0; + } + S_DEMO = true; + S_GRID = true; + show_demo(); + return 0; +} + +pub fn w_data(i: u32, s: []const u8) -> u32 { + if (i > 0) { return 0; } + if (s.len > 4194304) { + status_is(SAY_ERR_DEMO); + return 0; + } + var k : usize = 0; + while (k < s.len) { + D_BUF[k] = s[k]; + k = k + 1; + } + D_N = s.len as u32; + data_ready(); + return 0; +} + +pub fn w_data_failed(i: u32) -> u32 { + status_is(SAY_ERR_DEMO); + return 0; +} + +// --- a dropped file: the modules, the cells, tool.js's sort ------------------------------------------------------------------- + +/// The canonical array index a key spells ("0", "17"; not "01" or "4294967295"), else -1. +fn index_key(d: []const u8, klo: usize, khi: usize) -> i64 { + const e : u32 = jv_str_to(X_SCR[0..], 0, d, jspan(klo, khi)); + if (e == OVER or e == 0 or e > 10) { return 0 - 1; } + if (e > 1 and X_SCR[0] == 48) { return 0 - 1; } + var v : i64 = 0; + var k : u32 = 0; + while (k < e) { + const b : u8 = X_SCR[k as usize]; + if (!is_digit(b)) { return 0 - 1; } + v = v * 10 + ((b - 48) as i64); + k = k + 1; + } + if (v >= 4294967295) { return 0 - 1; } + return v; +} + +/// a goes before b, or they are equal: the order of the cell records (name, then module). +fn ent_le(d: []const u8, a: u32, b: u32) -> bool { + const c : i32 = key_cmp(d, E_KLO[a as usize] as usize, E_KHI[a as usize] as usize, E_KLO[b as usize] as usize, E_KHI[b as usize] as usize); + if (c < 0) { return true; } + if (c > 0) { return false; } + return E_RK[a as usize] <= E_RK[b as usize]; +} + +fn merge_pass(d: []const u8, src: []const u32, dst: []u32, n: u32, width: u32) -> u32 { + var i : u32 = 0; + while (i < n) { + var mid : u32 = i + width; + if (mid > n) { mid = n; } + var hi : u32 = i + width * 2; + if (hi > n) { hi = n; } + var a : u32 = i; + var b : u32 = mid; + var o : u32 = i; + while (a < mid and b < hi) { + if (ent_le(d, src[a as usize], src[b as usize])) { + dst[o as usize] = src[a as usize]; + a = a + 1; + } else { + dst[o as usize] = src[b as usize]; + b = b + 1; + } + o = o + 1; + } + while (a < mid) { + dst[o as usize] = src[a as usize]; + a = a + 1; + o = o + 1; + } + while (b < hi) { + dst[o as usize] = src[b as usize]; + b = b + 1; + o = o + 1; + } + i = hi; + } + return 0; +} + +/// Stable merge sort of E_IX[0..n] by ent_le. +fn sort_entries(d: []const u8, n: u32) -> u32 { + var i : u32 = 0; + while (i < n) { + E_IX[i as usize] = i; + i = i + 1; + } + var width : u32 = 1; + var in_ix : bool = true; + while (width < n) { + if (in_ix) { merge_pass(d, E_IX[0..], E_TM[0..], n, width); } else { merge_pass(d, E_TM[0..], E_IX[0..], n, width); } + in_ix = !in_ix; + width = width * 2; + } + if (!in_ix) { + i = 0; + while (i < n) { + E_IX[i as usize] = E_TM[i as usize]; + i = i + 1; + } + } + return 0; +} + +pub var E_NS : u32 = 0; + +/// Read the modules of the document: 0 ok (the cells with a BEL in E_TM[0..E_NS], in order), 1 not JSON, 2 no placed cells, +/// 3 too big for the tables. +fn scan_file(d: []const u8) -> u32 { + if (!jv_valid(d)) { return 1; } + const r : i64 = root(d); + E_N = 0; + E_NS = 0; + if (kind(d, r) != J_OBJECT) { return 2; } + const mods : i64 = obj_last(d, r, "modules"); + if (!jv_truthy(d, mods) or kind(d, mods) != J_OBJECT) { return 2; } + // the modules: a repeated key keeps its first place and its last value + MD_N = 0; + var p : usize = span_lo(mods) + 1; + const mhi : usize = span_hi(mods); + var go : bool = true; + while (go) { + const nx : i64 = mem_next(d, mhi, p); + if (nx < 0) { + go = false; + } else { + const klo : usize = M_KLO; + const khi : usize = M_KHI; + const vlo : usize = M_VLO; + const vhi : usize = M_VHI; + var found : i32 = 0 - 1; + var j : u32 = 0; + while (j < MD_N) { + if (key_cmp(d, klo, khi, MD_KLO[j as usize] as usize, MD_KHI[j as usize] as usize) == 0) { found = j as i32; } + j = j + 1; + } + if (found >= 0) { + MD_VLO[found as usize] = vlo as u32; + MD_VHI[found as usize] = vhi as u32; + } else { + if (MD_N >= W_MAX_MODS) { return 3; } + MD_KLO[MD_N as usize] = klo as u32; + MD_KHI[MD_N as usize] = khi as u32; + MD_VLO[MD_N as usize] = vlo as u32; + MD_VHI[MD_N as usize] = vhi as u32; + MD_NUM[MD_N as usize] = index_key(d, klo, khi); + MD_N = MD_N + 1; + } + p = nx as usize; + } + } + // Object.values order: integer keys ascending, then the others as they came + var nnum : u32 = 0; + var m : u32 = 0; + while (m < MD_N) { + if (MD_NUM[m as usize] >= 0) { nnum = nnum + 1; } + m = m + 1; + } + m = 0; + while (m < MD_N) { + var rk : u32 = 0; + var j2 : u32 = 0; + if (MD_NUM[m as usize] >= 0) { + while (j2 < MD_N) { + if (MD_NUM[j2 as usize] >= 0 and MD_NUM[j2 as usize] < MD_NUM[m as usize]) { rk = rk + 1; } + j2 = j2 + 1; + } + } else { + rk = nnum; + while (j2 < m) { + if (MD_NUM[j2 as usize] < 0) { rk = rk + 1; } + j2 = j2 + 1; + } + } + MD_RK[m as usize] = rk; + m = m + 1; + } + // the cells of every module: one record per entry + m = 0; + while (m < MD_N) { + const cells : i64 = obj_last(d, jspan(MD_VLO[m as usize] as usize, MD_VHI[m as usize] as usize), "cells"); + if (jv_truthy(d, cells) and kind(d, cells) == J_OBJECT) { + var cp : usize = span_lo(cells) + 1; + const chi : usize = span_hi(cells); + var cgo : bool = true; + while (cgo) { + const cn : i64 = mem_next(d, chi, cp); + if (cn < 0) { + cgo = false; + } else { + if (E_N >= W_MAX_CELLS) { return 3; } + const e : usize = E_N as usize; + E_KLO[e] = M_KLO as u32; + E_KHI[e] = M_KHI as u32; + E_RK[e] = MD_RK[m as usize]; + E_BSP[e] = NO_SPAN; + E_TSP[e] = NO_SPAN; + const c : i64 = jspan(M_VLO, M_VHI); + if (kind(d, c) == J_OBJECT) { + const attrs : i64 = obj_last(d, c, "attributes"); + if (kind(d, attrs) == J_OBJECT) { + const b : i64 = obj_last(d, attrs, "NEXTPNR_BEL"); + if (jv_truthy(d, b)) { + E_BSP[e] = b; + E_TSP[e] = obj_last(d, c, "type"); + } + } + } + E_N = E_N + 1; + cp = cn as usize; + } + } + } + m = m + 1; + } + sort_entries(d, E_N); + // a repeated cell name of one module keeps its last entry; only the entries with a BEL are cells + var i : u32 = 0; + var total : u32 = 0; + while (i < E_N) { + var last : bool = true; + const a : u32 = E_IX[i as usize]; + if (i + 1 < E_N) { + const b2 : u32 = E_IX[(i + 1) as usize]; + if (E_RK[a as usize] == E_RK[b2 as usize] and key_cmp(d, E_KLO[a as usize] as usize, E_KHI[a as usize] as usize, E_KLO[b2 as usize] as usize, E_KHI[b2 as usize] as usize) == 0) { last = false; } + } + if (last and E_BSP[a as usize] >= 0) { + E_TM[E_NS as usize] = a; + E_NS = E_NS + 1; + // will it fit the pool? (the three texts, decoded) + const nl : u32 = str_len(d, jspan(E_KLO[a as usize] as usize, E_KHI[a as usize] as usize)); + const e1 : u32 = jv_str(X_SCR[0..], 0, d, E_TSP[a as usize]); + const e2 : u32 = jv_str(X_SCR[0..], 0, d, E_BSP[a as usize]); + if (e1 == OVER or e2 == OVER) { return 3; } + if (total + nl + e1 + e2 > 33554432) { return 3; } + total = total + nl + e1 + e2; + } + i = i + 1; + } + if (E_NS == 0) { return 2; } + return 0; +} + +/// The surviving records into the cell pool: name (the key), String(type), String(BEL). +fn commit_cells(d: []const u8) -> u32 { + C_PN = 0; + C_N = E_NS; + var i : u32 = 0; + while (i < E_NS) { + const a : u32 = E_TM[i as usize]; + C_NO[i as usize] = C_PN; + C_PN = jv_str_to(C_POOL[0..], C_PN, d, jspan(E_KLO[a as usize] as usize, E_KHI[a as usize] as usize)); + C_NL[i as usize] = C_PN - C_NO[i as usize]; + C_TO[i as usize] = C_PN; + C_PN = jv_str(C_POOL[0..], C_PN, d, E_TSP[a as usize]); + C_TL[i as usize] = C_PN - C_TO[i as usize]; + C_BO[i as usize] = C_PN; + C_PN = jv_str(C_POOL[0..], C_PN, d, E_BSP[a as usize]); + C_BL[i as usize] = C_PN - C_BO[i as usize]; + C_FL[i as usize] = 0; + i = i + 1; + } + return 0; +} + +/// takeFile(file): name, size in bytes, the file's text as the host decoded it, st != 0 when it could not be read. +pub fn w_file(name: []const u8, size: f64, data: []const u8, st: u32) -> u32 { + S_CLR_FILE = true; + if (!S_GRID) { return 0; } + arg_text(0, name); + if (size > (K_MAX_FILE_MB as f64) * 1048576.0) { + arg_num(1, K_MAX_FILE_MB as f64); + status_fmt(SAY_ERR_SIZE, 2); + return 0; + } + if (size > (W_FILE_CAP as f64)) { + arg_num(1, W_FILE_CAP_MB as f64); + status_fmt(SAY_ERR_SIZE, 2); + return 0; + } + if (st != 0) { + status_fmt(SAY_ERR_JSON, 1); + return 0; + } + const rc : u32 = scan_file(data); + if (rc == 1) { + status_fmt(SAY_ERR_JSON, 1); + return 0; + } + if (rc == 2) { + status_fmt(SAY_ERR_NO_BELS, 1); + return 0; + } + if (rc == 3) { + arg_num(1, W_FILE_CAP_MB as f64); + status_fmt(SAY_ERR_SIZE, 2); + return 0; + } + commit_cells(data); + S_LABEL_N = copy_into(S_LABEL[0..], 0, name); + arg_loc(1, size); + const cr : i64 = obj_last(data, root(data), "creator"); + if (jv_truthy(data, cr)) { arg_val(2, data, cr, false); } else { arg_set(2, name, 0, 0); } + const sl : i64 = fmt_into(S_SRC[0..], SAY_SOURCE_FILE, 3); + S_SRC_N = 0; + if (sl >= 0) { S_SRC_N = sl as u32; } + S_CMDN = 0; + S_DEMO_HIDDEN = false; + return finish_load(); +} + +pub fn w_value(i: u32, dst: []u8) -> u32 { + if (i == 0 and S_CLR_FILE) { + S_CLR_FILE = false; + return 0; + } + return 4294967295; +} + +pub fn w_drag(kind_of: u32) -> u32 { + const was : bool = S_OVER; + S_OVER = kind_of == 0; + if (was != S_OVER) { return 1; } + return 0; +} + +// --- the canvas: size, fit, zoom -------------------------------------------------------------------------------------------------- + +/// size(): the canvas gets the width of its box and a height that fits the window. +fn size_canvas() -> u32 { + var bw : f64 = ENV_BW; + if (bw == 0.0 or bw != bw) { bw = 600.0; } + S_CSSW = bw; + const aspect : f64 = G_H / G_W; + var maxh : f64 = 0.0; + if (ENV_EMBED != 0) { + maxh = js_max(200.0, ENV_IH - 205.0); + } else { + maxh = js_max(260.0, ENV_IH * 0.7); + } + S_CSSH = js_round(js_min(S_CSSW * aspect, maxh)); + var dpr : f64 = ENV_DPR; + if (dpr == 0.0 or dpr != dpr) { dpr = 1.0; } + S_CANVAS_W = to_uint32(js_round(S_CSSW * dpr)); + S_CANVAS_H = to_uint32(js_round(S_CSSH * dpr)); + S_SIZED = true; + return 0; +} + +/// fit(design) +fn fit(design: bool) -> u32 { + if (!S_GRID) { return 0; } + size_canvas(); + var x0 : f64 = 0.0; + var y0 : f64 = 0.0; + var x1 : f64 = G_W; + var y1 : f64 = G_H; + if (design and BOX_ON) { + x0 = BOX_X0; + y0 = BOX_Y0; + x1 = BOX_X1; + y1 = BOX_Y1; + } + const s : f64 = js_min(S_CSSW / (x1 - x0), S_CSSH / (y1 - y0)); + S_VS = s; + S_VX = (x0 + x1) / 2.0 - S_CSSW / s / 2.0; + S_VY = (y0 + y1) / 2.0 - S_CSSH / s / 2.0; + return 0; +} + +/// zoomAt(px, py, factor) +fn zoom_at(px: f64, py: f64, factor: f64) -> u32 { + if (!S_GRID) { return 0; } + const x : f64 = S_VX; + const y : f64 = S_VY; + const s : f64 = S_VS; + const mins : f64 = js_min(S_CSSW / G_W, S_CSSH / G_H) * 0.8; + const ns : f64 = js_min(js_max(s * factor, mins), mins * (K_MAX_ZOOM as f64) * 4.0); + const gx : f64 = x + px / s; + const gy : f64 = y + py / s; + S_VS = ns; + S_VX = gx - px / ns; + S_VY = gy - py / ns; + return 0; +} + +/// nearest(px, py): the placed cell within K_PICK_PX, the smaller one when several are under the point; -1 for none. +fn nearest(px: f64, py: f64) -> i32 { + const x : f64 = S_VX; + const y : f64 = S_VY; + const s : f64 = S_VS; + var best : i32 = 0 - 1; + var bd : f64 = K_PICK_PX as f64; + var i : u32 = 0; + while (i < P_N) { + const k : usize = i as usize; + const cx : f64 = (P_X[k] + P_W[k] / 2.0 - x) * s; + const cy : f64 = (P_Y[k] + P_H[k] / 2.0 - y) * s; + const ddx : f64 = js_max(0.0, abs_f(px - cx) - P_W[k] * s / 2.0); + const ddy : f64 = js_max(0.0, abs_f(py - cy) - P_H[k] * s / 2.0); + const dd : f64 = hypot2(ddx, ddy); + var take : bool = dd < bd; + if (!take and dd == 0.0 and best >= 0) { + if (P_W[k] * P_H[k] < P_W[best as usize] * P_H[best as usize]) { take = true; } + } + if (take) { + bd = dd; + best = i as i32; + } + i = i + 1; + } + return best; +} + +fn pt_find(id: i64) -> i32 { + var i : u32 = 0; + while (i < PT_N) { + if (PT_ID[i as usize] == id) { return i as i32; } + i = i + 1; + } + return 0 - 1; +} + +fn pt_remove(id: i64) -> u32 { + const f : i32 = pt_find(id); + if (f < 0) { return 0; } + var i : u32 = f as u32; + while (i + 1 < PT_N) { + PT_ID[i as usize] = PT_ID[(i + 1) as usize]; + PT_X[i as usize] = PT_X[(i + 1) as usize]; + PT_Y[i as usize] = PT_Y[(i + 1) as usize]; + i = i + 1; + } + PT_N = PT_N - 1; + return 0; +} + +/// A pointer on the canvas. kind 0 down, 1 move, 2 up, 3 cancel, 4 leave; x, y in the canvas's box. +pub fn w_cpointer(kind_of: u32, id: i64, x: f64, y: f64, mouse: u32) -> u32 { + if (kind_of == 0) { + const f : i32 = pt_find(id); + if (f >= 0) { + PT_X[f as usize] = x; + PT_Y[f as usize] = y; + } else { + if (PT_N < 8) { + PT_ID[PT_N as usize] = id; + PT_X[PT_N as usize] = x; + PT_Y[PT_N as usize] = y; + PT_N = PT_N + 1; + } + } + if (PT_N == 1) { + DR_ON = true; + DR_SX = x; + DR_SY = y; + DR_VX = S_VX; + DR_VY = S_VY; + DR_VS = S_VS; + DR_MOVED = false; + } + if (PT_N == 2) { + PI_ON = true; + PI_D = hypot2(PT_X[0] - PT_X[1], PT_Y[0] - PT_Y[1]); + PI_S = S_VS; + DR_ON = false; + } + return 0; + } + if (kind_of == 1) { + const f2 : i32 = pt_find(id); + if (f2 >= 0) { + PT_X[f2 as usize] = x; + PT_Y[f2 as usize] = y; + } + if (PI_ON and PT_N == 2) { + const mx : f64 = (PT_X[0] + PT_X[1]) / 2.0; + const my : f64 = (PT_Y[0] + PT_Y[1]) / 2.0; + zoom_at(mx, my, (PI_S * hypot2(PT_X[0] - PT_X[1], PT_Y[0] - PT_Y[1]) / PI_D) / S_VS); + return 0; + } + if (DR_ON) { + const dx : f64 = x - DR_SX; + const dy : f64 = y - DR_SY; + if (abs_f(dx) + abs_f(dy) > 4.0) { DR_MOVED = true; } + if (DR_MOVED) { + S_VS = DR_VS; + S_VX = DR_VX - dx / DR_VS; + S_VY = DR_VY - dy / DR_VS; + } + return 0; + } + if (mouse != 0) { + const p : i32 = nearest(x, y); + if (p != S_HOVER) { + S_HOVER = p; + S_INFO = true; + } + } + return 0; + } + if (kind_of == 2) { + if (DR_ON and !DR_MOVED) { + S_PICK = nearest(x, y); + S_INFO = true; + } + pt_remove(id); + if (PT_N < 2) { PI_ON = false; } + if (PT_N == 0) { DR_ON = false; } + return 0; + } + if (kind_of == 3) { + pt_remove(id); + PI_ON = false; + DR_ON = false; + return 0; + } + if (S_HOVER >= 0) { + S_HOVER = 0 - 1; + S_INFO = true; + } + return 0; +} + +/// The wheel over the canvas: zoomAt(x, y, Math.exp(-deltaY * (ctrl ? 0.01 : 0.0025))). 2: a libm call is missing. +pub fn w_wheel(x: f64, y: f64, dy: f64, ctrl: u32) -> u32 { + MISSING = false; + var k : f64 = 0.0025; + if (ctrl != 0) { k = 0.01; } + const arg : f64 = (0.0 - dy) * k; + const f : f64 = libm(16, arg); + if (MISSING) { return 2; } + zoom_at(x, y, f); + return 1; +} + +pub fn w_key(key: []const u8) -> u32 { + const step : f64 = 40.0 / S_VS; + if (key.len == 1 and (key[0] == 43 or key[0] == 61)) { + zoom_at(S_CSSW / 2.0, S_CSSH / 2.0, 1.4); + return 1; + } + if (key.len == 1 and key[0] == 45) { + zoom_at(S_CSSW / 2.0, S_CSSH / 2.0, 1.0 / 1.4); + return 1; + } + if (same_text(key, "ArrowLeft")) { + S_VX = S_VX - step; + return 1; + } + if (same_text(key, "ArrowRight")) { + S_VX = S_VX + step; + return 1; + } + if (same_text(key, "ArrowUp")) { + S_VY = S_VY - step; + return 1; + } + if (same_text(key, "ArrowDown")) { + S_VY = S_VY + step; + return 1; + } + return 0; +} + +/// The ResizeObserver of the box around the canvas: 1 when the drawing changed. +pub fn w_box(w: f64, h: f64) -> u32 { + if (!S_GRID or w == S_LASTW) { return 0; } + const ow : f64 = S_CSSW; + const oh : f64 = S_CSSH; + const ovx : f64 = S_VX; + const ovy : f64 = S_VY; + const ovs : f64 = S_VS; + S_LASTW = w; + size_canvas(); + if (ow != 0.0 and ow == ow) { + const cx : f64 = ovx + ow / ovs / 2.0; + const cy : f64 = ovy + oh / ovs / 2.0; + const s : f64 = ovs * S_CSSW / ow; + S_VS = s; + S_VX = cx - S_CSSW / s / 2.0; + S_VY = cy - S_CSSH / s / 2.0; + } + return 1; +} + +pub fn w_click(i: u32) -> u32 { + if (i == 0) { fit(false); } + if (i == 1) { fit(true); } + if (i == 2) { show_demo(); } + return 0; +} + +// --- the libm service ---------------------------------------------------------------------------------------------------------------- + +fn libm(op: u32, a: f64) -> f64 { + var i : u32 = 0; + while (i < LB_N) { + if ((LB_OP[i as usize] as u32) == op and LB_A[i as usize] == a) { return LB_R[i as usize]; } + i = i + 1; + } + MISSING = true; + var j : u32 = 0; + while (j < RQ_N) { + if ((RQ_OP[j as usize] as u32) == op and RQ_A[j as usize] == a) { return 0.0; } + j = j + 1; + } + if (RQ_N < 64) { + RQ_OP[RQ_N as usize] = op as u8; + RQ_A[RQ_N as usize] = a; + RQ_N = RQ_N + 1; + } + return 0.0; +} + +pub fn w_libm_need(dst: []f64) -> u32 { + if (RQ_N == 0) { return 0; } + var k : u32 = 0; + while (k < RQ_N and ((k * 3 + 2) as usize) < dst.len) { + dst[(k * 3) as usize] = RQ_OP[k as usize] as f64; + dst[(k * 3 + 1) as usize] = RQ_A[k as usize]; + dst[(k * 3 + 2) as usize] = 0.0; + k = k + 1; + } + RQ_SENT = k; + return k; +} + +pub fn w_libm_give(src: []const f64, n: u32) -> u32 { + var k : u32 = 0; + while (k < n and k < RQ_SENT and (k as usize) < src.len) { + if (LB_N >= 64) { LB_N = 0; } + LB_OP[LB_N as usize] = RQ_OP[k as usize]; + LB_A[LB_N as usize] = RQ_A[k as usize]; + LB_R[LB_N as usize] = src[k as usize]; + LB_N = LB_N + 1; + k = k + 1; + } + RQ_N = 0; + RQ_SENT = 0; + MISSING = false; + return 0; +} + +// --- the page: HTML as tool.js builds it ------------------------------------------------------------------------------------------------ + +fn put_say(dst: []u8, n0: u32, tpl: []const u8, nargs: u32) -> u32 { + const l : i64 = fmt_into(S_FB[0..], tpl, nargs); + if (l < 0) { return OVER; } + return text(dst, n0, S_FB[0..(l as usize)]); +} + +fn put_locale(dst: []u8, n0: u32, x: f64) -> u32 { + return js_locale_num(dst, n0, x); +} + +/// Text from the data as the page shows it: escaped like text(), and a carriage return as a character reference, which an HTML +/// parser keeps (a raw one it turns into a line feed, tool.js keeps the carriage return). +fn etext(dst: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = n0; + var i : usize = 0; + while (i < s.len) { + if (s[i] == 13) { n = raw(dst, n, " "); } else { n = put_escaped(dst, n, s[i]); } + i = i + 1; + } + return n; +} + +fn pool_text(dst: []u8, n0: u32, o: u32, l: u32) -> u32 { + return etext(dst, n0, C_POOL[(o as usize)..((o + l) as usize)]); +} + +/// as h('i', { style: `background:${KIND_COLOR[kind]}` }) writes it +fn put_swatch(dst: []u8, n0: u32, k: u32) -> u32 { + var n : u32 = raw(dst, n0, ""); +} + +fn put_legend(dst: []u8, n0: u32) -> u32 { + var n : u32 = n0; + var i : usize = 0; + while (i < K_KINDS.len and i < 16) { + if (K_CNT[i] > 0) { + n = raw(dst, n, "
  • "); + n = put_swatch(dst, n, i as u32); + n = text(dst, n, K_KINDS[i]); + n = raw(dst, n, " "); + n = put_locale(dst, n, K_CNT[i] as f64); + n = raw(dst, n, "
  • "); + } + i = i + 1; + } + return n; +} + +/// renderInfo(): the pick, else the hover, as a table of the cells that share its site. +fn put_info(dst: []u8, n0: u32) -> u32 { + var n : u32 = n0; + var pi : i32 = S_PICK; + if (pi < 0) { pi = S_HOVER; } + if (pi < 0) { + n = raw(dst, n, "

    "); + n = text(dst, n, SAY_PICK_NONE); + return raw(dst, n, "

    "); + } + const ci : u32 = P_C[pi as usize]; + var same : u32 = 0; + var q : u32 = 0; + while (q < P_N) { + if (site_same(P_C[q as usize], ci)) { same = same + 1; } + q = q + 1; + } + arg_num(0, same as f64); + n = raw(dst, n, "

    "); + n = put_say(dst, n, SAY_PICK_LINE, 1); + n = raw(dst, n, "

    "); + var f : usize = 0; + while (f < SAY_FIELDS.len) { + n = raw(dst, n, ""); + f = f + 1; + } + n = raw(dst, n, ""); + var shown : u32 = 0; + q = 0; + while (q < P_N and shown < W_ROWS_SHOWN) { + const cj : u32 = P_C[q as usize]; + if (site_same(cj, ci)) { + shown = shown + 1; + n = raw(dst, n, ""); + } + q = q + 1; + } + return raw(dst, n, "
    "); + n = text(dst, n, SAY_FIELDS[f]); + n = raw(dst, n, "
    "); + n = put_swatch(dst, n, P_K[q as usize] as u32); + if ((C_FL[cj as usize] % 2) == 0) { n = pool_text(dst, n, C_NO[cj as usize], C_NL[cj as usize]); } + n = raw(dst, n, ""); + if (((C_FL[cj as usize] / 2) % 2) == 0) { n = pool_text(dst, n, C_TO[cj as usize], C_TL[cj as usize]); } + n = raw(dst, n, ""); + n = pool_text(dst, n, C_BO[cj as usize], C_BL[cj as usize]); + n = raw(dst, n, "
    "); +} + +fn put_details(dst: []u8, n0: u32) -> u32 { + var n : u32 = raw(dst, n0, "
    "); + } else { + if (O_N == 0) { n = raw(dst, n, " hidden=\"\""); } + n = raw(dst, n, ">"); + arg_loc(0, O_N as f64); + n = put_say(dst, n, SAY_OFF_MAP, 1); + n = raw(dst, n, "
      "); + var j : u32 = 0; + while (j < O_N and j < W_OFF_SHOWN) { + const cj : u32 = O_C[j as usize]; + n = raw(dst, n, "
    • "); + n = pool_text(dst, n, C_NO[cj as usize], C_NL[cj as usize]); + n = raw(dst, n, " "); + n = pool_text(dst, n, C_TO[cj as usize], C_TL[cj as usize]); + n = raw(dst, n, " "); + if (((C_FL[cj as usize] / 4) % 2) == 1) { n = text(dst, n, W_DASH); } else { n = pool_text(dst, n, C_BO[cj as usize], C_BL[cj as usize]); } + n = raw(dst, n, "
    • "); + j = j + 1; + } + n = raw(dst, n, "
    "); + } + n = raw(dst, n, "
    "); + } else { + if (S_CMDN == 0) { n = raw(dst, n, " hidden=\"\""); } + n = raw(dst, n, ">"); + n = text(dst, n, SAY_COMMANDS); + n = raw(dst, n, "

    "); + n = etext(dst, n, S_SRC[0..(S_SRC_N as usize)]); + n = raw(dst, n, "

      "); + var c : u32 = 0; + while (c < S_CMDN) { + n = raw(dst, n, "
    • "); + n = etext(dst, n, S_CMD[(S_CMDO[c as usize] as usize)..((S_CMDO[c as usize] + S_CMDL[c as usize]) as usize)]); + n = raw(dst, n, "
    • "); + c = c + 1; + } + n = raw(dst, n, "
    "); + } + return n; +} + +fn button(dst: []u8, n0: u32, label: []const u8) -> u32 { + var n : u32 = raw(dst, n0, ""); +} + +/// A CSS length in px as the style serialisation writes it: 273px, 12.5px. +fn put_css_px(dst: []u8, n0: u32, x: f64) -> u32 { + const e : u32 = js_str_num(S_NUM3[0..], 0, x); + var n : u32 = raw(dst, n0, S_NUM3[0..(e as usize)]); + return raw(dst, n, "px"); +} + +pub fn w_render(dst: []u8) -> u32 { + var n : u32 = raw(dst, 0, "
    "); + n = button(dst, n, SAY_WHOLE_DIE); + n = button(dst, n, SAY_FIT_DESIGN); + n = raw(dst, n, "

    "); + n = etext(dst, n, S_STATUS[0..(S_STATUS_N as usize)]); + n = raw(dst, n, "

    "); + n = text(dst, n, SAY_HINT); + n = raw(dst, n, "

      "); + if (S_SIDE) { n = put_legend(dst, n); } + n = raw(dst, n, "
    "); + if (S_INFO) { n = put_info(dst, n); } + n = raw(dst, n, "
    "); + n = put_details(dst, n); + if (!ok(n)) { return 0; } + return n; +} + +// --- the binary draw list (op byte, then f64 little endian / u16-length strings; 200 picks canvas i) ---------------------------------------- + +fn bw_b(o: []u8, n: u32, b: u8) -> u32 { + if (n == OVER or (n as usize) >= o.len) { return OVER; } + o[n as usize] = b; + return n + 1; +} + +/// The IEEE 754 bits of a finite double (no subnormals, no NaN). +fn f64_bits(x: f64) -> u64 { + if (x == 0.0) { + if ((1.0 / x) < 0.0) { return 9223372036854775808; } + return 0; + } + var sign : u64 = 0; + var ax : f64 = x; + if (x < 0.0) { + sign = 9223372036854775808; + ax = 0.0 - x; + } + if (x != x) { return 9221120237041090560; } + if (ax - ax != 0.0) { return sign + 9218868437227405312; } + const m : u64 = jn_mant(ax); + const e : i32 = jn_expo(ax); + const be : u64 = ((e + 52 + 1023) as u64); + return sign + be * 4503599627370496 + (m - 4503599627370496); +} + +fn bw_f(o: []u8, n0: u32, x: f64) -> u32 { + var bits : u64 = f64_bits(x); + var n : u32 = n0; + var k : u32 = 0; + while (k < 8) { + n = bw_b(o, n, (bits % 256) as u8); + bits = bits / 256; + k = k + 1; + } + return n; +} + +fn bw_s(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = bw_b(o, n0, (s.len % 256) as u8); + n = bw_b(o, n, ((s.len / 256) % 256) as u8); + var k : usize = 0; + while (k < s.len) { + n = bw_b(o, n, s[k]); + k = k + 1; + } + return n; +} + +fn bw_rect(o: []u8, n0: u32, op: u8, a: f64, b: f64, c: f64, d: f64) -> u32 { + var n : u32 = bw_b(o, n0, op); + n = bw_f(o, n, a); + n = bw_f(o, n, b); + n = bw_f(o, n, c); + return bw_f(o, n, d); +} + +fn bw_set_s(o: []u8, n0: u32, op: u8, s: []const u8) -> u32 { + const n : u32 = bw_b(o, n0, op); + return bw_s(o, n, s); +} + +fn kind_color(k: u32) -> []const u8 { + if ((k as usize) < K_KIND_COLORS.len) { return K_KIND_COLORS[k as usize]; } + return W_KIND_DEFAULT; +} + +/// The ring around the hovered or picked cell. +fn bw_ring(o: []u8, n0: u32, pi: i32, col: []const u8, s: f64, vx: f64, vy: f64) -> u32 { + const k : usize = pi as usize; + var n : u32 = bw_set_s(o, n0, 33, col); + n = bw_b(o, n, 34); + n = bw_f(o, n, 2.0); + const w : f64 = js_max(8.0, P_W[k] * s); + const hh : f64 = js_max(8.0, P_H[k] * s); + const x : f64 = (P_X[k] - vx) * s; + const y : f64 = (P_Y[k] - vy) * s; + return bw_rect(o, n, 11, x + P_W[k] * s / 2.0 - w / 2.0 - 2.0, y + P_H[k] * s / 2.0 - hh / 2.0 - 2.0, w + 4.0, hh + 4.0); +} + +/// draw(): the die floor, the placed cells, the rings. +pub fn w_bdraw(o: []u8) -> u32 { + if (!S_GRID or !S_SIZED) { return 0; } + var n : u32 = bw_b(o, 0, 200); + n = bw_b(o, n, 0); + var dpr : f64 = ENV_DPR; + if (dpr == 0.0 or dpr != dpr) { dpr = 1.0; } + n = bw_b(o, n, 12); + n = bw_f(o, n, dpr); + n = bw_f(o, n, 0.0); + n = bw_f(o, n, 0.0); + n = bw_f(o, n, dpr); + n = bw_f(o, n, 0.0); + n = bw_f(o, n, 0.0); + n = bw_set_s(o, n, 32, W_BLACK); + n = bw_rect(o, n, 10, 0.0, 0.0, S_CSSW, S_CSSH); + const vx : f64 = S_VX; + const vy : f64 = S_VY; + const s : f64 = S_VS; + var r : u32 = 0; + while (r < GR_N) { + const x : f64 = (GR_GX[r as usize] - vx) * s; + if (!(x > S_CSSW) and !(x + s < 0.0)) { + const t : i32 = GR_K[r as usize]; + if (t >= 0 and (t as usize) < K_TILE_COLORS.len) { n = bw_set_s(o, n, 32, K_TILE_COLORS[t as usize]); } else { n = bw_set_s(o, n, 32, W_TILE_DEFAULT); } + n = bw_rect(o, n, 10, x, (GR_TOP[r as usize] - vy) * s, js_max(1.0, s * 0.9), js_max(1.0, (GR_BOT[r as usize] - GR_TOP[r as usize] + 1.0) * s - js_min(1.0, s * 0.1))); + } + r = r + 1; + } + var i : u32 = 0; + while (i < P_N) { + const k : usize = i as usize; + var px : f64 = (P_X[k] - vx) * s; + var py : f64 = (P_Y[k] - vy) * s; + var pw : f64 = P_W[k] * s; + var ph : f64 = P_H[k] * s; + if (!(px > S_CSSW) and !(py > S_CSSH) and !(px + pw < 0.0 - 3.0) and !(py + ph < 0.0 - 3.0)) { + if (pw < 3.0) { + px = px - (3.0 - pw) / 2.0; + pw = 3.0; + } + if (ph < 3.0) { + py = py - (3.0 - ph) / 2.0; + ph = 3.0; + } + n = bw_set_s(o, n, 32, kind_color(P_K[k] as u32)); + n = bw_rect(o, n, 10, px, py, pw, ph); + } + i = i + 1; + } + if (S_HOVER >= 0) { n = bw_ring(o, n, S_HOVER, W_HOVER_COLOR, s, vx, vy); } + if (S_PICK >= 0) { n = bw_ring(o, n, S_PICK, W_PICK_COLOR, s, vx, vy); } + if (n == OVER) { return 0; } + return n; +} + + +// --- Tests of the native page ------------------------------------------------------------------------------------------- + +pub const W_TEST_DATA : str = "{\"source\":{\"top\":\"t<1>\",\"design\":\"d\",\"part\":\"p\",\"nextpnr\":\"n\",\"commands\":[\"c1 & c2\",\"c3\"]},\"geometry\":{\"grid_w\":40,\"grid_h\":30,\"runs\":[[\"clbll\",2,1,20],[\"bram\",10,3,12],[\"zzz\",11,0,0]],\"maps\":{\"SLICE\":{\"x\":[2,2,3,3],\"y\":[1,2,3,4,5,6]},\"RAMB36\":{\"x\":[10],\"y\":[8,16]},\"RAMB18\":{\"x\":[10],\"y\":[8,12,16,20]},\"IOB\":{\"x\":[0],\"y\":[5,6]},\"BUFGCTRL\":{\"x\":[20],\"y\":[2]}}},\"cells\":[[\"a\",\"SLICE_LUTX\",\"SLICE_X0Y0/A6LUT\"],[\"b\",\"SLICE_FFX\",\"SLICE_X1Y0/CFF\"],[\"c\",\"CARRY4\",\"SLICE_X0Y1/CARRY4\"],[\"r\",\"RAMB36E1\",\"RAMB36_X0Y1/RAMB36E1\"],[\"f\",\"FIFO18E1\",\"FIFO18_X0Y2/FIFO18E1\"],[\"i\",\"IOB33_OUTBUF\",\"IOB_X0Y1/IOB33/OUTBUF\"],[\"g\",\"BUFGCTRL\",\"BUFGCTRL_X0Y0/BUFGCTRL\"],[\"z\",\"PSEUDO_GND\",\"NULL_X0Y9/PSEUDO_GND_BEL\"],[\"q\",\"SLICE_LUTX\",\"SLICE_X0Y0/B5LUT\"],[\"w\",\"FOO\",\"SLICE_X9Y0/A6LUT\"],[null,null,null]]}"; + +fn has_text(o: []const u8, n: u32, want: []const u8) -> bool { + if (want.len > (n as usize)) { return false; } + var i : usize = 0; + while (i + want.len <= (n as usize)) { + var k : usize = 0; + while (k < want.len and o[i + k] == want[k]) { k = k + 1; } + if (k == want.len) { return true; } + i = i + 1; + } + return false; +} + +fn open_page() -> u32 { + w_init(""); + w_view(800.0, 1.0, 0, 900.0, 800.0); + return 0; +} + +fn load_test() -> u32 { + w_data(0, W_TEST_DATA); + return 0; +} + +test the_page_opens_loading_with_nothing_drawn { + open_page(); + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert n > 0; + assert has_text(o[0..], n, "
    "); + assert has_text(o[0..], n, "

    Loading the placement...

    "); + assert has_text(o[0..], n, "
    "); + assert has_text(o[0..], n, "
    "); + assert has_text(o[0..], n, "
  • a b& FOO NULL_X0Y1/B
  • "); +} + +test a_type_longer_than_the_old_scratch_buffer_is_read { + open_page(); + load_test(); + var f : [40000]u8 = [0; 40000]; + const head : []const u8 = "{\"modules\":{\"t\":{\"cells\":{\"a\":{\"type\":\""; + const tail : []const u8 = "\",\"attributes\":{\"NEXTPNR_BEL\":\"NULL_X0Y1/B\"}}}}}}"; + var n : usize = 0; + var k : usize = 0; + while (k < head.len) { + f[n] = head[k]; + n = n + 1; + k = k + 1; + } + var q : usize = 0; + while (q < 20000) { + f[n] = 116; + n = n + 1; + q = q + 1; + } + k = 0; + while (k < tail.len) { + f[n] = tail[k]; + n = n + 1; + k = k + 1; + } + assert w_file("g.json", 20100.0, f[0..n], 0) == 0; + assert C_N == 1 and C_TL[0] == 20000; +} + +/// The splicer follows only the names a module itself mentions, not the names the imported functions mention. +/// This anchor mentions every helper the writer, the JSON reader and the number rules call. Never called. +pub fn imports_anchor(o: []u8, s: []const u8) -> u32 { + var n : u32 = 0; + n = n + (ok(0) as u32); + n = n + OVER; + n = n + put(o, 0, 0); + n = n + raw(o, 0, s); + n = n + put_escaped(o, 0, 0); + n = n + text(o, 0, s); + n = n + num(o, 0, 0); + n = n + attr(o, 0, s, s); + n = n + fill(o, 0, s, s, s, s, 0); + n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s); + n = n + dec_to(o, 0); + n = n + (name_is(s, 0, 0, s) as u32); + n = n + (is_word_byte(0) as u32); + n = n + (H_LT as u32); + n = n + (H_GT as u32); + n = n + (H_AMP as u32); + n = n + (H_QUOTE as u32); + n = n + (H_APOS as u32); + n = n + (H_SPACE as u32); + n = n + (H_SLASH as u32); + n = n + (H_EQ as u32); + n = n + ((NO_SPAN < 0) as u32); + n = n + ((SPAN > 0) as u32); + n = n + (J_NONE as u32); + n = n + (J_OBJECT as u32); + n = n + (J_ARRAY as u32); + n = n + (J_STRING as u32); + n = n + (J_NUMBER as u32); + n = n + (J_TRUE as u32); + n = n + (J_FALSE as u32); + n = n + (J_NULL as u32); + n = n + (jspan(0, 0) as u32); + n = n + (span_lo(0) as u32); + n = n + (span_hi(0) as u32); + n = n + (skip_ws(s, 0) as u32); + n = n + (string_end(s, 0) as u32); + n = n + (value_end(s, 0) as u32); + n = n + (root(s) as u32); + n = n + (kind(s, 0) as u32); + n = n + (key_is(s, 0, 0, s) as u32); + n = n + (obj_get(s, 0, s) as u32); + n = n + arr_len(s, 0); + n = n + (arr_at(s, 0, 0) as u32); + n = n + hex4(s, 0); + n = n + escape_at(s, 0, 0); + n = n + cp_utf8_len(0); + n = n + (cp_utf8_byte(0, 0) as u32); + n = n + str_len(s, 0); + n = n + (str_byte(s, 0, 0) as u32); + n = n + JN_OVER; + n = n + JN_CAP; + n = n + ((JN_TWO52 > 0.0) as u32); + n = n + ((JN_TWO53 > 0.0) as u32); + n = n + jn_put(o, 0, 0); + n = n + jn_raw(o, 0, s); + n = n + jn_digits(o, 0); + n = n + jn_mul(o, 0, 0); + n = n + jn_mul2(o, 0, 0); + n = n + jn_mul5(o, 0, 0); + n = n + jn_shift(o, 0, 0); + n = n + (jn_cmp(s, s, 0) as u32); + n = n + (jn_expo(0.0) as u32); + n = n + (jn_mant(0.0) as u32); + n = n + ((jn_make(0, 0) > 0.0) as u32); + n = n + jn_exact(o, 0, 0); + n = n + jn_fscale(0); + n = n + (js_is_nan(0.0) as u32); + n = n + ((js_nan(0.0) > 0.0) as u32); + n = n + ((js_inf(1.0) > 0.0) as u32); + n = n + ((js_floor(0.0) > 0.0) as u32); + n = n + ((js_ceil(0.0) > 0.0) as u32); + n = n + ((js_max(0.0, 0.0) > 0.0) as u32); + n = n + ((js_min(0.0, 0.0) > 0.0) as u32); + n = n + ((js_round(0.0) > 0.0) as u32); + n = n + (jn_shortest(o, 1.0) as u32); + n = n + jn_copy(o, s, 0); + n = n + jn_put_run(o, 0, s, 0, 0); + n = n + jn_layout(o, 0, s, 0, 0); + n = n + js_str_num(o, 0, 0.0); + n = n + js_to_fixed(o, 0, 0.0, 0); + n = n + js_locale_num(o, 0, 0.0); + n = n + (jn_num_ok(s, 0, 0) as u32); + n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32); + n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32); + n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32); + n = n + (jn_abs(0) as u32); + n = n + js_str_u64(o, 0, 0); + n = n + jv_str_to(o, 0, s, 0); + n = n + jv_text_str(o, 0, s, 0); + n = n + ((jv_numval(s, 0) > 0.0) as u32); + n = n + jv_str(o, 0, s, 0); + n = n + jv_text(o, 0, s, 0); + n = n + (jv_is(s, 0, s) as u32); + n = n + (jv_truthy(s, 0) as u32); + n = n + (jv_is_ws(0) as u32); + n = n + ((jv_number_of_text(s, 0) > 0.0) as u32); + n = n + ((jv_num(s, 0) > 0.0) as u32); + n = n + jv_vput_str(o, 0, s, s); + n = n + jv_vput_f64(o, 0, s, 0.0); + n = n + jv_vput_val(o, 0, s, s, 0); + n = n + jv_vput_defined(o, 0, s, s, 0); + n = n + jv_vput_fixed(o, 0, s, 0.0, 0); + n = n + jv_kid(o, 0, s, 0); + n = n + jv_find(s, 0, s); + n = n + jv_fill(o, 0, s, s, 0, true); + n = n + (jv_string_ok(s, 0) as u32); + n = n + (jv_number_ok(s, 0) as u32); + n = n + (jv_lit_ok(s, 0, s) as u32); + n = n + (jv_valid(s) as u32); + n = n + jv_ws_at(s, 0, 0); + n = n + jv_ws_before(s, 0, 0); + n = n + jv_digit_value(0); + n = n + jv_fill_raw(o, 0, s, s, 0, true); + n = n + jv_fill_with(o, 0, s, s, 0, true, true); + n = n + num_grouped(o, 0, 0); + return n; +} diff --git a/specs/widgets/slack_waterfall.t27 b/specs/widgets/slack_waterfall.t27 new file mode 100644 index 0000000000..cc963df7ae --- /dev/null +++ b/specs/widgets/slack_waterfall.t27 @@ -0,0 +1,2394 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/slack-waterfall.t27 -- every timing path of a routed design, worst first +; Source of truth for public/widgets/slack-waterfall/ (gHashTag/trinity, apps/website). The page +; is written by scripts/widget-pages-from-spec.mjs from this file and hands every constant below +; to tool.js as window.T27_WIDGET. tool.js holds no English of its own: every word on screen is a +; SAY_ constant here, every number the tool relies on is a K_ constant. +; ASCII only (L3), English only (LANG-EN). + +; WHAT IT DOES: one row per timing endpoint, sorted by slack, worst at the top. Each row is the +; worst path into that endpoint drawn as a waterfall of its segments -- clock-to-Q, logic through +; each LUT or carry, routing on each net, setup -- against the clock period; failing paths are +; red. Tap a row for the path cell by cell. Drop a nextpnr --report JSON and it draws that +; report's critical_paths (the mainline nextpnr schema: path segments of type clk-to-q, source, +; logic, routing, setup with delays in ns), or a timing.json written by fpga-pnr.py for your own +; run. The file is read in the browser with FileReader and sent nowhere. + +; DATA PROVENANCE: timing.json is written by scripts/widget-data/fpga-pnr.py from one real run +; made on 2026-10-04: trinity_v3_jtaguart.v from gHashTag/trinity-fpga de4ebda367, yosys +; 0.67+post b8e7da6f40, nextpnr-xilinx 0.9.2-107-g7037c948 on the xc7a200tfbg676-1 chipdb with +; --freq 50 --seed 1. The board's XDC declares its 50 MHz clock on the pad, but this nextpnr +; does not carry it through the IBUF and BUFGCTRL to the clock net, so without --freq the run is +; checked at its 12 MHz default; --freq 50 restates the board's 50 MHz. This nextpnr-xilinx build +; writes --report with critical_paths always empty (only fmax and utilization), so fpga-pnr.py +; walks the run's own SDF (--sdf: INTERCONNECT and IOPATH delays, SETUPHOLD setup) the way +; nextpnr's common/timing.cc walks the netlist, and then checks itself against nextpnr's log: the +; worst endpoint, its 4.8 ns total and every net on its path must equal the critical path the log +; prints, and the slack histogram rebuilt with nextpnr's binning must equal the log's, all 20 +; bins. If any check fails the script writes nothing. Results: 159 endpoints, worst path 4767 ps +; (2245 logic, 2522 routing), worst slack 15233 ps at 50 MHz, nextpnr fmax 209.78 MHz. + +; WHAT IT DOES NOT CLAIM: that the design was measured at speed on a board; these are nextpnr's +; delay models. Hold time, clock skew and IO timing are not analysed: nextpnr-xilinx treats the +; IO buffers of this flow as untimed, so pads start and end no path here. Only the worst path +; into each endpoint is drawn, not every path. A mainline report lists one critical path per +; clock pair, so a dropped report shows those, not every endpoint. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_slack_waterfall; + +use widgets::html_out::ok; +use widgets::html_out::OVER; +use widgets::html_out::put; +use widgets::html_out::raw; +use widgets::html_out::put_escaped; +use widgets::html_out::text; +use widgets::html_out::num; +use widgets::html_out::num_grouped; +use widgets::html_out::attr; +use widgets::html_out::fill; +use widgets::html_out::fill_named; +use widgets::html_out::dec_to; +use widgets::html_out::name_is; +use widgets::html_out::is_word_byte; +use widgets::html_out::H_LT; +use widgets::html_out::H_GT; +use widgets::html_out::H_AMP; +use widgets::html_out::H_QUOTE; +use widgets::html_out::H_APOS; +use widgets::html_out::H_SPACE; +use widgets::html_out::H_SLASH; +use widgets::html_out::H_EQ; +use widgets::json_scan::NO_SPAN; +use widgets::json_scan::SPAN; +use widgets::json_scan::J_NONE; +use widgets::json_scan::J_OBJECT; +use widgets::json_scan::J_ARRAY; +use widgets::json_scan::J_STRING; +use widgets::json_scan::J_NUMBER; +use widgets::json_scan::J_TRUE; +use widgets::json_scan::J_FALSE; +use widgets::json_scan::J_NULL; +use widgets::json_scan::jspan; +use widgets::json_scan::span_lo; +use widgets::json_scan::span_hi; +use widgets::json_scan::skip_ws; +use widgets::json_scan::string_end; +use widgets::json_scan::value_end; +use widgets::json_scan::root; +use widgets::json_scan::kind; +use widgets::json_scan::key_is; +use widgets::json_scan::obj_get; +use widgets::json_scan::arr_len; +use widgets::json_scan::arr_at; +use widgets::json_scan::hex4; +use widgets::json_scan::escape_at; +use widgets::json_scan::cp_utf8_len; +use widgets::json_scan::cp_utf8_byte; +use widgets::json_scan::str_len; +use widgets::json_scan::str_byte; +use widgets::js_num::JN_OVER; +use widgets::js_num::JN_CAP; +use widgets::js_num::JN_TWO52; +use widgets::js_num::JN_TWO53; +use widgets::js_num::jn_put; +use widgets::js_num::jn_raw; +use widgets::js_num::jn_digits; +use widgets::js_num::jn_mul; +use widgets::js_num::jn_mul2; +use widgets::js_num::jn_mul5; +use widgets::js_num::jn_shift; +use widgets::js_num::jn_cmp; +use widgets::js_num::jn_expo; +use widgets::js_num::jn_mant; +use widgets::js_num::jn_make; +use widgets::js_num::jn_exact; +use widgets::js_num::jn_fscale; +use widgets::js_num::js_is_nan; +use widgets::js_num::js_nan; +use widgets::js_num::js_inf; +use widgets::js_num::js_floor; +use widgets::js_num::js_ceil; +use widgets::js_num::js_max; +use widgets::js_num::js_min; +use widgets::js_num::js_round; +use widgets::js_num::jn_shortest; +use widgets::js_num::jn_copy; +use widgets::js_num::jn_put_run; +use widgets::js_num::jn_layout; +use widgets::js_num::js_str_num; +use widgets::js_num::js_to_fixed; +use widgets::js_num::js_locale_num; +use widgets::js_num::jn_num_ok; +use widgets::js_num::jn_cmp_text; +use widgets::js_num::js_parse_num; +use widgets::js_num::jn_dec_value; +use widgets::js_num::jn_abs; +use widgets::js_num::js_str_u64; +use widgets::js_val::jv_str_to; +use widgets::js_val::jv_text_str; +use widgets::js_val::jv_numval; +use widgets::js_val::jv_str; +use widgets::js_val::jv_text; +use widgets::js_val::jv_is; +use widgets::js_val::jv_truthy; +use widgets::js_val::jv_is_ws; +use widgets::js_val::jv_number_of_text; +use widgets::js_val::jv_num; +use widgets::js_val::jv_vput_str; +use widgets::js_val::jv_vput_f64; +use widgets::js_val::jv_vput_val; +use widgets::js_val::jv_vput_defined; +use widgets::js_val::jv_vput_fixed; +use widgets::js_val::jv_kid; +use widgets::js_val::jv_find; +use widgets::js_val::jv_fill; +use widgets::js_val::jv_string_ok; +use widgets::js_val::jv_number_ok; +use widgets::js_val::jv_lit_ok; +use widgets::js_val::jv_valid; +use widgets::js_val::jv_ws_at; +use widgets::js_val::jv_ws_before; +use widgets::js_val::jv_digit_value; +use widgets::js_val::jv_fill_raw; +use widgets::js_val::jv_fill_with; + +pub const KIND : str = "widget-tool"; +pub const ID : str = "slack-waterfall"; +pub const TITLE : str = "Slack waterfall: every timing path of a routed design, worst first"; +pub const DESCRIPTION : str = "159 endpoints of a real nextpnr-xilinx run at 50 MHz as logic and routing waterfalls, checked bin by bin against nextpnr's log. Drop a --report JSON for yours."; +pub const IMAGE_ALT : str = "A waterfall on black: 159 rows, one per timing endpoint, sorted worst first, each a strip of gold logic and green routing segments ending well before a red 20 ns period line, the spare time shaded dark green. Headline: worst slack +15.233 ns, fmax 209.78 MHz."; +pub const HOOK : str = "Where do 4.8 ns go in a 20 ns clock? Every path of a real Artix-7 run, segment by segment, worst first; drop your report for yours."; +pub const CATEGORY : str = "fpga"; +pub const DATA_SOURCES : [5]str = ["gHashTag/trinity-fpga de4ebda367: fpga/openxc7-synth/trinity_v3_jtaguart.v and jtaguart.xdc", "nextpnr-xilinx 0.9.2-107-g7037c948 --chipdb xc7a200tfbg676-1.bin --freq 50 --seed 1: routed.sdf, report.json (fmax) and the log's critical path and slack histogram", "scripts/widget-data/fpga-pnr.py: per-endpoint paths from the SDF, checked against the log, written to timing.json", "nextpnr common/timing.cc and xilinx/arch.cc (nextpnr-xilinx 0.9.2-107-g7037c948): the arrival, setup and port-class rules followed", "A nextpnr --report JSON or a fpga-pnr.py timing.json the reader drops or picks: read in the browser with FileReader, never uploaded"]; +pub const READS_LOCAL_FILES : bool = true; +pub const SENDS_NOTHING : bool = true; + +; --- Numbers the tool relies on ------------------------------------------------------------ +pub const K_DATA_FILE : str = "timing.json"; +pub const K_SCHEMA : str = "slack-waterfall/1"; +; Segment types and their colours; mainline nextpnr uses the same type names. +pub const K_SEG_TYPES : [6]str = ["clk-to-q", "source", "logic", "routing", "setup", "input"]; +pub const K_SEG_COLORS : [6]str = ["#ffd700", "#ffd700", "#ffd700", "#00ff88", "#c8c8c8", "#ffd700"]; +pub const K_SLACK_COLOR : str = "#002e1c"; +pub const K_FAIL_COLOR : str = "#ff4d6d"; +pub const K_PS_PER_NS : u16 = 1000; +; Row height of the waterfall in CSS pixels, before it is squeezed to fit, and the floor. +pub const K_ROW_PX : u8 = 7; +pub const K_MIN_ROW_PX : u8 = 2; +pub const K_MAX_CANVAS_PX : u16 = 560; +pub const K_MIN_TARGET_PX : u8 = 32; +pub const K_MAX_FILE_MB : u16 = 256; +; The demo, as fpga-pnr.py measured it; the tool refuses a demo file that disagrees. +pub const K_DEMO_FREQ_MHZ : u8 = 50; +pub const K_DEMO_PERIOD_PS : u32 = 20000; +pub const K_DEMO_ENDPOINTS : u16 = 159; +pub const K_DEMO_WORST_TOTAL_PS : u32 = 4767; +pub const K_DEMO_WORST_LOGIC_PS : u32 = 2245; +pub const K_DEMO_WORST_ROUTING_PS : u32 = 2522; +pub const K_DEMO_WORST_SLACK_PS : u32 = 15233; + +; --- Words on screen ------------------------------------------------------------------------ +; Templates take {0}, {1}, ... in order. +pub const SAY_FULL_PERIOD : str = "Full period"; +pub const SAY_FIT_PATHS : str = "Fit paths"; +pub const SAY_WORSE : str = "Worse"; +pub const SAY_BETTER : str = "Better"; +pub const SAY_DEMO : str = "Demo design"; +pub const SAY_SUMMARY : str = "{0}: {1} paths, worst slack {2} ns at {3} MHz"; +pub const SAY_SUMMARY_NO_PERIOD : str = "{0}: {1} paths, worst total {2} ns, no clock constraint in the file"; +pub const SAY_FMAX_HEAD : [4]str = ["clock", "fmax MHz", "target MHz", ""]; +pub const SAY_PASS : str = "PASS"; +pub const SAY_FAIL : str = "FAIL"; +pub const SAY_CHECK_LINE : str = "Checked against nextpnr's log: worst path {0} ns ending at {1} (SDF walk: {2} ps), {3} of {4} histogram bins equal."; +pub const SAY_NO_PATHS : str = "{0} has no critical_paths: this nextpnr-xilinx build writes fmax only. Run scripts/widget-data/fpga-pnr.py on its --sdf and log for every path."; +pub const SAY_CANVAS_LABEL : str = "Waterfall of timing paths, worst slack at the top: gold is logic, green is routing, grey is setup, dark green is slack, the red line is the clock period. Tap a row for its path."; +pub const SAY_HINT : str = "tap or click a row: its path / arrow keys: step"; +pub const SAY_AXIS : str = "{0} ns"; +pub const SAY_PATH_TITLE : str = "Path {0} of {1}: {2}"; +pub const SAY_PATH_LINE : str = "total {0} ns = {1} logic + {2} routing; slack {3} ns"; +pub const SAY_PATH_LINE_NO_PERIOD : str = "total {0} ns = {1} logic + {2} routing"; +pub const SAY_SEG_HEAD : [5]str = ["type", "ns", "from", "to", "net"]; +pub const SAY_LEGEND : [5]str = ["logic", "routing", "setup", "slack", "period"]; +pub const SAY_DROP : str = "Drop a nextpnr --report JSON or a fpga-pnr.py timing.json here, or tap to pick one. It is read in this browser and sent nowhere."; +pub const SAY_LOADING : str = "Loading the paths..."; +pub const SAY_READING : str = "Reading {0}..."; +pub const SAY_ERR_JSON : str = "{0} is not JSON."; +pub const SAY_ERR_SHAPE : str = "{0} is neither a nextpnr report (critical_paths, fmax) nor a timing.json."; +pub const SAY_ERR_SIZE : str = "{0} is larger than {1} MB."; +pub const SAY_ERR_DEMO : str = "The demo paths did not load."; +pub const SAY_ERR_DEMO_DIFF : str = "The demo file disagrees with this page's spec; not drawn."; +pub const SAY_COMMANDS : str = "How this was made"; + +; --- Tests ---------------------------------------------------------------------------------- +test the_period_is_the_inverse_of_the_frequency { + assert K_DEMO_PERIOD_PS * K_DEMO_FREQ_MHZ == 1000000; + assert K_PS_PER_NS == 1000; +} + +test the_worst_path_adds_up { + assert K_DEMO_WORST_LOGIC_PS + K_DEMO_WORST_ROUTING_PS == K_DEMO_WORST_TOTAL_PS; + assert K_DEMO_WORST_TOTAL_PS + K_DEMO_WORST_SLACK_PS == K_DEMO_PERIOD_PS; + assert K_DEMO_WORST_TOTAL_PS < K_DEMO_PERIOD_PS; +} + +test the_worst_path_rounds_to_nextpnr_log_figures { + assert 48 == (K_DEMO_WORST_TOTAL_PS + 50) / 100; + assert 22 == (K_DEMO_WORST_LOGIC_PS + 50) / 100; + assert 25 == (K_DEMO_WORST_ROUTING_PS + 50) / 100; +} + +test every_segment_type_has_a_colour { + assert K_SEG_TYPES[3] == "routing"; + assert K_SEG_COLORS[3] == "#00ff88"; + assert K_SEG_TYPES[4] == "setup"; +} + +test the_page_reads_files_and_sends_nothing { + assert READS_LOCAL_FILES == true; + assert SENDS_NOTHING == true; + assert same_text(CATEGORY, "fpga"); +} + + +; ================================================================================================= +; THE NATIVE PAGE (epic gHashTag/t27#10937, issue #10938, stage "page" of specs/t27f/widget_factory.t27) +; Everything below replaces public/widgets/slack-waterfall/tool.js (370 lines of hand-written JavaScript). The +; words and numbers above are unchanged (but for one test, see the end of this header). The page keeps its state +; in module-level `pub var`s, writes the page as HTML and the waterfall as a BINARY draw list (the protocol of +; fasm_skyline.t27, plus the property codes 36 textBaseline, 37 textAlign and 38 globalAlpha that idcode.t27 adds); +; the one shared host (host.js) sets the HTML, replays the list, forwards the events and runs the effects. +; Exports (all `pub fn`): +; w_init(hash) the page opens +; w_view(width, dpr, embed, innerHeight, boxWidth) the numbers tool.js reads from the window and from the box +; .sw-wrap (w_measure_sel), stamped by the host before every event +; w_measure_sel(dst) ".sw-wrap" w_scroll_sel(dst) ".sw-wrap": its scrollTop survives a repaint +; w_canvas_keep() the canvas is one element for the life of the page (focus) +; w_drop_root() a file dropped anywhere on the page is taken (tool.js listens on the page root) +; w_draw_cap() the byte size of the draw list buffer w_file_cap() the biggest file text handed over +; w_data(i, bytes) timing.json was fetched; w_data_failed(i) +; w_click(i) the i-th button: 0 Full period, 1 Fit paths, 2 Worse, 3 Better, 4 Demo design +; w_key(key) a key on the canvas: 1 when it was one of the six tool.js handles (the host then calls +; preventDefault, as tool.js does) +; w_cpointer(kind, id, x, y, mouse) pointer events on the canvas, x and y relative to it: kind 0 is the press +; w_box(w, h) the ResizeObserver of the box: 1 when the canvas must be sized and drawn again +; w_drag(kind) 0 over the drop label, 1 left it, 2 dropped: 1 when its look changed +; w_file(name, size, text, st) a file was picked or dropped; st 1 when the host could not read it +; w_value(i, dst) the text the file field must hold (empty, after a pick: tool.js clears it) or 4294967295 +; w_render(dst) the page's HTML w_bdraw(dst) the waterfall as a binary draw list (canvas 0) +; w_text_need(dst), w_text_give(widths, n) the text width service (see stat_card.t27): ctx.measureText of the +; axis labels; font strings carry the CSS-variable marker (byte 4, name, byte 5, fallback, +; byte 6), which is `getComputedStyle(root).getPropertyValue(name) || fallback`, NOT trimmed +; w_libm_need(dst), w_libm_give(src, n) Math.log10 and Math.pow of the axis step, answered by the host +; w_effect(k, dst) effects: record 121 ("y") "sel LF y LF rowH" scroll the box so the picked row shows, +; as scrollToPick(); record 120 ("x") "sel" set the box's scrollTop to 0 +; STATE tool.js keeps in DOM elements and in its closure is kept as flags: what each element last held. The part +; of the page render() rewrites is a function of the data; the parts it does not touch (the detail table when the +; data has no path, the canvas size and the box's max-height when the data has no path, a status text set by a +; failed drop) keep their old look, as there: DET_N holds the last detail table. +; The page reads a file the way tool.js does: the first character-for-character JSON.parse verdict (jv_valid), the +; two shapes (the timing.json of fpga-pnr.py, a nextpnr --report), and the cases where tool.js throws halfway (a +; null path, a segment that is not an array, a clock entry that is null: the status stays "Reading NAME..."). +; Not carried over from JavaScript (said plainly; the measurement does not cover them): +; - files: the host hands over a file's text up to W_FILE_CAP bytes (8 MiB); a bigger file says SAY_ERR_SIZE with +; its real limit (W_FILE_MB), where tool.js reads up to 256 MB. timing.json is read up to 1 MiB, W_MAX_PATHS (65536) +; paths and W_MAX_FMAX (256) clocks are kept, W_MAX_CELL (4096) bytes of one cell text; +; - a number is expected where tool.js adds or compares ("ps", "slack", "period_ps", fmax numbers): a string there +; adds as text in JavaScript; "kind" and the strings table may hold other kinds (shown as String(v)); +; - arrays inside arrays where h() flattens them (a segment kind that is an array); a segment row that is a string +; (JavaScript reads its characters); "strings" that is not an array; the clock name "constructor" and the other +; names of Object.prototype read as clocks of the report's table; integer-like clock names (JavaScript lists +; them first); a repeated key inside a JSON object reads as the first here, the last in JSON.parse; +; - src.commands that is not an array (JavaScript throws halfway through render()); a cell text above W_MAX_CELL; +; - the box of the canvas is observed on the page root (the ResizeObserver of tool.js watches the box itself); +; "Reading NAME..." is not shown while the file is read (the host hands the whole text over at once); +; - tool.js never reads the box width while the box is hidden; the host stamps it before the event, so the first +; drawing may use 600 for one pass until the observer reports the real width; +; - an infinite loop: if the widest path is 0 ps long the axis step is 0 and tool.js never leaves its loop; here +; the axis is left out. +; One test of the spec, the_page_reads_files_and_sends_nothing, reads same_text(CATEGORY, "fpga") where it read CATEGORY == "fpga": +; t27c 0.5.2 does not compile a comparison of two str constants. + +; MEASURED 2026-10-20: t27c gen + a 24-line Zig export wrapper + zig build-exe wasm32-freestanding ReleaseSmall give a +; 42255-byte wasm (with 17 MiB of tables in zero-initialised memory). The original page (tool.js) and this one ran in two +; DOM windows (happy-dom) with a FAKE getContext('2d') on each that records every method call and every property +; assignment (measureText answers one proportional stand-in), a fake ResizeObserver, and stand-ins for clientWidth, +; clientHeight, scrollTop, innerHeight and devicePixelRatio; after every action the DOM (attributes, text, the scrollTop of +; the box), the preventDefault of every key and, whenever tool.js drew, its last call sequence against the native page's +; last one were compared: 4151 comparisons (16.6 million calls and assignments recorded, 2169 drawings of tool.js), 0 +; differences. They cover the real timing.json with every button and key, pointer presses in and out of the rows, 10 +; widths from 0 to 2400, devicePixelRatio 0 to 3, embed on and off, the font variable, scrolling and the details; 79 dropped +; or picked files (28 timing.json and 24 nextpnr reports of generated shapes: no, one, many paths, with and without +; a period, slack null / negative / missing, segment kinds of every sort, clocks with null, string and missing numbers, a +; check and a source of every shape; reports with no paths, only fmax, an fmax array; 23 files that are refused or make +; tool.js throw halfway), 8 x 6 files again with 45 random events each, 16 demo files (404, not JSON, a demo that +; disagrees in the period, in the path count and in the slack, root null / array / number / {}, a null path, a null clock, +; label and source changes), and events before the demo arrives. Negative controls: a changed DOM node, a changed scrollTop, a rect +; digit and a property value moved by 1e-9, a skipped call, a wrong CSS fallback, a measured width 1000 too wide, no scrollTop +; kept through a repaint, no scroll to the picked row, a key not prevented and a wrong libm function are each reported. +; NOT compared (happy-dom has no layout, fonts or pixels): the pixels, real font widths (a stand-in answers), the real +; ResizeObserver (a fake one is fired by hand), focus, pointer capture, the real FileReader error cases, and white space around +; the --w-mono variable (happy-dom trims custom property values, so the untrimmed read is by construction only). + +pub const W_FILE_CAP : u32 = 8388608; +pub const W_FILE_MB : u32 = 8; +pub const W_DEMO_CAP : u32 = 1048576; +pub const W_DRAW_CAP : u32 = 4194304; +pub const W_MAX_PATHS : u32 = 65536; +pub const W_MAX_FMAX : u32 = 256; +pub const W_MAX_CELL : u32 = 4096; +pub const W_DET_CAP : u32 = 262144; +pub const W_SEL : str = ".sw-wrap"; +pub const W_AXIS : f64 = 18.0; +pub const W_MONO_VAR : str = "--w-mono"; +pub const W_MONO_FB : str = "monospace"; +pub const W_LINE_FB : str = "var(--w-line)"; +pub const W_GOLD_FB : str = "#ffd700"; + +pub var DEMO : [1048576]u8 = [0; 1048576]; +pub var DEMO_LEN : u32 = 0; +pub var FA : [8388608]u8 = [0; 8388608]; +pub var FB : [8388608]u8 = [0; 8388608]; +pub var CUR : u32 = 0; +pub var DEMO_OK : bool = false; +pub var FULL : bool = true; +pub var PICK : u32 = 0; +pub var ST : u32 = 0; +pub var ST_FAIL : bool = false; +pub var ST_NAME : [1100]u8 = [0; 1100]; +pub var ST_NAME_LEN : u32 = 0; +pub var RENDERED : bool = false; +pub var OVER_DROP : bool = false; +pub var CLR_FILE : bool = false; +pub var EF : [4]u8 = [0; 4]; +pub var EF_N : u32 = 0; +pub var ENV_W : f64 = 0.0; +pub var ENV_DPR : f64 = 1.0; +pub var ENV_EMBED : u32 = 0; +pub var ENV_IH : f64 = 768.0; +pub var ENV_BW : f64 = 0.0; +pub var LAST_W : f64 = 0.0; +pub var CV_SET : bool = false; +pub var CV_W : u64 = 0; +pub var CV_H : u64 = 0; +pub var CV_SH : u64 = 0; +pub var CV_MAXH : u64 = 0; +pub var CSS_W : f64 = 0.0; +pub var ROW_H : f64 = 3.0; +pub var X_MAX : f64 = 1.0; +pub var DET_LEN : u32 = 0; +pub var DET : [262144]u8 = [0; 262144]; + +// the data in use: tool.js's state.data, read from the bytes of the buffer CUR names +pub var DS_FMT : u32 = 0; +pub var DS_N : u32 = 0; +pub var DS_PERIOD : f64 = 0.0; +pub var DS_HAS_PERIOD : bool = false; +pub var DS_LABEL : [1100]u8 = [0; 1100]; +pub var DS_LABEL_LEN : u32 = 0; +pub var DS_ROOT : i64 = -1; +pub var PSP : [65536]i64 = [0; 65536]; +pub var ORD : [65536]u32 = [0; 65536]; +pub var TMPO : [65536]u32 = [0; 65536]; +pub var TOT : [65536]f64 = [0.0; 65536]; +pub var ROU : [65536]f64 = [0.0; 65536]; +pub var SLK : [65536]f64 = [0.0; 65536]; +pub var SLN : [65536]u8 = [0; 65536]; +pub var FM_N : u32 = 0; +pub var FM_K : [256]i64 = [0; 256]; +pub var FM_A : [256]i64 = [0; 256]; +pub var FM_C : [256]i64 = [0; 256]; +pub var FM_KIDX : [256]u32 = [0; 256]; + +// the text width memo and the libm memo (answers the host gives; a miss makes the list wrong and is asked again) +pub var X_N : u32 = 0; +pub var X_FO : [256]u32 = [0; 256]; +pub var X_FL : [256]u32 = [0; 256]; +pub var X_TO : [256]u32 = [0; 256]; +pub var X_TL : [256]u32 = [0; 256]; +pub var X_ST : [256]u8 = [0; 256]; +pub var X_W : [256]f64 = [0.0; 256]; +pub var X_POOL : [16384]u8 = [0; 16384]; +pub var X_POOL_N : u32 = 0; +pub var X_MISS : bool = false; +pub var LB_N : u32 = 0; +pub var LB_OP : [64]u8 = [0; 64]; +pub var LB_A : [64]f64 = [0.0; 64]; +pub var LB_B : [64]f64 = [0.0; 64]; +pub var LB_R : [64]f64 = [0.0; 64]; +pub var RQ_N : u32 = 0; +pub var RQ_OP : [64]u8 = [0; 64]; +pub var RQ_A : [64]f64 = [0.0; 64]; +pub var RQ_B : [64]f64 = [0.0; 64]; +pub var RQ_SENT : u32 = 0; +pub var MISSING : bool = false; + +// --- small pieces ------------------------------------------------------------------------------------------ + +fn copy_into(dst: []u8, at: u32, s: []const u8) -> u32 { + var n : u32 = at; + var i : usize = 0; + while (i < s.len and (n as usize) < dst.len) { + dst[n as usize] = s[i]; + n = n + 1; + i = i + 1; + } + return n; +} + +/// The active buffer: 1 the demo, 2 and 3 the two file buffers. +fn ds_bytes() -> []const u8 { + if (CUR == 1) { return DEMO[0..(DEMO_LEN as usize)]; } + if (CUR == 2) { return FA[0..(FA_LEN as usize)]; } + return FB[0..(FB_LEN as usize)]; +} + +pub var FA_LEN : u32 = 0; +pub var FB_LEN : u32 = 0; + +/// String(v) of the value at sp into tmp (cut where tmp ends); its length. +fn str_of(tmp: []u8, d: []const u8, sp: i64) -> u32 { + const l : u32 = jv_str(tmp, 0, d, sp); + if (l == OVER) { return 0; } + return l; +} + +/// A child of h(): missing, null and false add nothing, the rest is String(v) as escaped text. +fn put_kid(o: []u8, n0: u32, d: []const u8, sp: i64) -> u32 { + const k : u8 = kind(d, sp); + if (k == J_NONE or k == J_NULL or k == J_FALSE) { return n0; } + return jv_text(o, n0, d, sp); +} + +/// Number(v) of the value at sp, the way `a + b` and `a < b` read a number (a missing value is NaN). +fn numv(d: []const u8, sp: i64) -> f64 { + if (kind(d, sp) == J_NONE) { return js_nan(0.0); } + return jv_num(d, sp); +} + +fn ns_of(ps: f64) -> f64 { + return ps / (K_PS_PER_NS as f64); +} + +/// ns(ps): (ps / PS).toFixed(3). +fn put_ns(o: []u8, n0: u32, ps: f64) -> u32 { + return js_to_fixed(o, n0, ns_of(ps), 3); +} + +/// signed(ps): "+" for ps >= 0 (a null slack counts as 0), then ns(ps). +fn put_signed(o: []u8, n0: u32, ps: f64, is_null: bool) -> u32 { + var n : u32 = n0; + var v : f64 = ps; + if (is_null) { v = 0.0; } + if (v >= 0.0) { n = put(o, n, 43); } + return put_ns(o, n, v); +} + +/// +(x).toFixed(digits) as String(): the number the text reads back as. +fn put_fixed_num(o: []u8, n0: u32, x: f64, digits: u32) -> u32 { + var tmp : [400]u8 = [0; 400]; + const l : u32 = js_to_fixed(tmp[0..], 0, x, digits); + if (l == OVER) { return OVER; } + const v : f64 = js_parse_num(tmp[0..], 0, l as usize); + return js_str_num(o, n0, v); +} + +fn dec_text(tmp: []u8, v: u64) -> u32 { + return dec_to(tmp, v); +} + +// --- reading the two shapes --------------------------------------------------------------------------------- + +/// The value after the last "." of nothing: `str(i)` of fromOwn(): i >= 0 ? String(s[i] ?? '') : '', raw bytes. +fn put_str_idx(o: []u8, n0: u32, d: []const u8, strs: i64, idx: i64) -> u32 { + const k : u8 = kind(d, idx); + if (k != J_NUMBER) { return n0; } + const v : f64 = jv_numval(d, idx); + if (!(v >= 0.0)) { return n0; } + if (v != js_floor(v) or v > 4294967000.0) { return n0; } + const el : i64 = arr_at(d, strs, v as u32); + const ek : u8 = kind(d, el); + if (ek == J_NONE or ek == J_NULL) { return n0; } + return jv_str(o, n0, d, el); +} + +/// The value at sp is an array for `x.map(...)`; a falsy value stands for [] (`x || []`); other kinds throw (false). +fn list_ok(d: []const u8, sp: i64) -> bool { + const k : u8 = kind(d, sp); + if (k == J_ARRAY) { return true; } + return !jv_truthy(d, sp); +} + +/// Does the element at sp destructure as an array in `([a, b, ...]) =>`: an array does; a string is read as its +/// characters in JavaScript (not carried over); the others throw. +fn row_ok(d: []const u8, sp: i64) -> bool { + const k : u8 = kind(d, sp); + return k == J_ARRAY or k == J_STRING; +} + +/// Object.entries(x) for the value at sp into FM_*: an object gives its members, an array its index as the key. +/// Returns false when an entry's value is null and tool.js dereferences it (own: v.achieved). +fn fmax_entries(d: []const u8, sp: i64, own: bool, store: bool) -> bool { + if (store) { FM_N = 0; } + if (!jv_truthy(d, sp)) { return true; } + const k : u8 = kind(d, sp); + if (k == J_OBJECT) { + const hi : usize = span_hi(sp); + var p : usize = skip_ws(d, span_lo(sp) + 1); + + while (p < hi and d[p] == 34 and (!store or FM_N < W_MAX_FMAX)) { + const ke : usize = string_end(d, p); + const q : usize = skip_ws(d, ke); + const v : usize = skip_ws(d, q + 1); + const ve : usize = value_end(d, v); + const vs : i64 = jspan(v, ve); + if (own and kind(d, vs) == J_NULL) { return false; } + if (store) { + FM_K[FM_N as usize] = jspan(p, ke); + FM_A[FM_N as usize] = obj_get(d, vs, "achieved"); + FM_C[FM_N as usize] = obj_get(d, vs, "constraint"); + FM_KIDX[FM_N as usize] = 0; + FM_N = FM_N + 1; + } + + p = skip_ws(d, ve); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + return true; + } + if (k == J_ARRAY) { + const cnt : u32 = arr_len(d, sp); + var i : u32 = 0; + while (i < cnt and (!store or FM_N < W_MAX_FMAX)) { + const vs : i64 = arr_at(d, sp, i); + if (own and kind(d, vs) == J_NULL) { return false; } + if (store) { + FM_K[FM_N as usize] = NO_SPAN; + FM_A[FM_N as usize] = obj_get(d, vs, "achieved"); + FM_C[FM_N as usize] = obj_get(d, vs, "constraint"); + FM_KIDX[FM_N as usize] = i; + FM_N = FM_N + 1; + } + i = i + 1; + } + } + return true; +} + +/// The key of fmax entry i as raw text into tmp. +fn fm_key_raw(tmp: []u8, d: []const u8, i: u32) -> u32 { + if (FM_K[i as usize] == NO_SPAN) { return dec_to(tmp, FM_KIDX[i as usize] as u64); } + return jv_str_to(tmp, 0, d, FM_K[i as usize]); +} + +/// Would tool.js throw while it builds the data of a timing.json (fromOwn) from the object at r? +fn own_throws(d: []const u8, r: i64) -> bool { + const ps : i64 = obj_get(d, r, "paths"); + var hi : usize = 0; + var p : usize = 0; + if (kind(d, ps) == J_ARRAY) { + hi = span_hi(ps); + p = skip_ws(d, span_lo(ps) + 1); + } + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + const sp : i64 = jspan(p, e); + if (kind(d, sp) == J_NULL) { return true; } + const segs : i64 = obj_get(d, sp, "segs"); + if (!list_ok(d, segs)) { return true; } + if (kind(d, segs) == J_ARRAY) { + const sh : usize = span_hi(segs); + var q : usize = skip_ws(d, span_lo(segs) + 1); + while (q < sh and d[q] != 93) { + const qe : usize = value_end(d, q); + if (!row_ok(d, jspan(q, qe))) { return true; } + q = skip_ws(d, qe); + if (q < sh and d[q] == 44) { q = skip_ws(d, q + 1); } + } + } + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + return !fmax_entries(d, obj_get(d, r, "fmax"), true, false); +} + +/// Would tool.js throw while it builds the data of a nextpnr report (fromReport)? +fn report_throws(d: []const u8, r: i64) -> bool { + const cps : i64 = obj_get(d, r, "critical_paths"); + if (!list_ok(d, cps)) { return true; } + if (kind(d, cps) == J_ARRAY) { + const hi : usize = span_hi(cps); + var p : usize = skip_ws(d, span_lo(cps) + 1); + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + const cp : i64 = jspan(p, e); + if (kind(d, cp) == J_NULL) { return true; } + const path : i64 = obj_get(d, cp, "path"); + if (!list_ok(d, path)) { return true; } + if (kind(d, path) == J_ARRAY) { + const sh : usize = span_hi(path); + var q : usize = skip_ws(d, span_lo(path) + 1); + while (q < sh and d[q] != 93) { + const qe : usize = value_end(d, q); + if (kind(d, jspan(q, qe)) == J_NULL) { return true; } + q = skip_ws(d, qe); + if (q < sh and d[q] == 44) { q = skip_ws(d, q + 1); } + } + } + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + } + return !fmax_entries(d, obj_get(d, r, "fmax"), false, false); +} + +/// The segment list of the path at sp, or NO_SPAN: own "segs", report "path". +fn segs_of(d: []const u8, sp: i64) -> i64 { + if (DS_FMT == 1) { return obj_get(d, sp, "segs"); } + return obj_get(d, sp, "path"); +} + +/// A segment's kind value, its time in ps and whether it is routing: own [kind, ps, ...], report {type, delay, ...}. +fn seg_kind(d: []const u8, g: i64) -> i64 { + if (DS_FMT == 1) { return arr_at(d, g, 0); } + return obj_get(d, g, "type"); +} + +fn seg_ps(d: []const u8, g: i64) -> f64 { + if (DS_FMT == 1) { return numv(d, arr_at(d, g, 1)); } + const dl : i64 = obj_get(d, g, "delay"); + var v : f64 = 0.0; + if (jv_truthy(d, dl)) { v = numv(d, dl); } + return js_round(v * (K_PS_PER_NS as f64)); +} + +fn seg_is_routing(d: []const u8, g: i64) -> bool { + return jv_is(d, seg_kind(d, g), "routing"); +} + +/// colorOf(kind): the colour of the type the value equals, else the colour of "logic". +fn put_color(o: []u8, n0: u32, d: []const u8, ksp: i64) -> u32 { + var i : usize = 0; + while (i < K_SEG_TYPES.len) { + if (jv_is(d, ksp, K_SEG_TYPES[i])) { return jn_raw(o, n0, K_SEG_COLORS[i]); } + i = i + 1; + } + return jn_raw(o, n0, K_SEG_COLORS[2]); +} + +/// The key a path sorts by: slack ?? -total. +fn sort_key(i: u32) -> f64 { + if (SLN[i as usize] != 0) { return 0.0 - TOT[i as usize]; } + return SLK[i as usize]; +} + +/// ORD[0..n] in file order, then a stable merge sort by sort_key (Array.prototype.sort is stable). +fn sort_paths(cnt: u32) -> u32 { + var i : u32 = 0; + while (i < cnt) { + ORD[i as usize] = i; + i = i + 1; + } + var w : u32 = 1; + while (w < cnt) { + var lo : u32 = 0; + while (lo < cnt) { + var mid : u32 = lo + w; + if (mid > cnt) { mid = cnt; } + var hi : u32 = lo + w + w; + if (hi > cnt) { hi = cnt; } + var a : u32 = lo; + var b : u32 = mid; + var k : u32 = lo; + while (a < mid and b < hi) { + if (sort_key(ORD[b as usize]) - sort_key(ORD[a as usize]) < 0.0) { + TMPO[k as usize] = ORD[b as usize]; + b = b + 1; + } else { + TMPO[k as usize] = ORD[a as usize]; + a = a + 1; + } + k = k + 1; + } + while (a < mid) { + TMPO[k as usize] = ORD[a as usize]; + a = a + 1; + k = k + 1; + } + while (b < hi) { + TMPO[k as usize] = ORD[b as usize]; + b = b + 1; + k = k + 1; + } + lo = lo + w + w; + } + var c : u32 = 0; + while (c < cnt) { + ORD[c as usize] = TMPO[c as usize]; + c = c + 1; + } + w = w + w; + } + return 0; +} + +/// Is String(a) equal to the decoded key of fmax entry i. +fn fm_key_is(d: []const u8, i: u32, v: i64) -> bool { + var a : [1100]u8 = [0; 1100]; + var b : [1100]u8 = [0; 1100]; + const al : u32 = fm_key_raw(a[0..], d, i); + const bl : u32 = str_of(b[0..], d, v); + if (al != bl) { return false; } + var k : u32 = 0; + while (k < al) { + if (a[k as usize] != b[k as usize]) { return false; } + k = k + 1; + } + return true; +} + +/// cons[key] ?? ...: the constraint of the clock named by the value at v, or NO_SPAN. +fn fm_constraint_of(d: []const u8, v: i64) -> i64 { + var i : u32 = 0; + var res : i64 = NO_SPAN; + while (i < FM_N) { + if (fm_key_is(d, i, v)) { res = FM_C[i as usize]; } + i = i + 1; + } + return res; +} + +fn present(d: []const u8, sp: i64) -> bool { + const k : u8 = kind(d, sp); + return k != J_NONE and k != J_NULL; +} + +/// Build the data of the active buffer: DS_*, the paths' totals and the sorted order. fmt 1 timing.json, 2 report. +/// The checks of own_throws / report_throws have passed. +fn build_ds(fmt: u32, label: []const u8) -> u32 { + DS_FMT = fmt; + DS_LABEL_LEN = copy_into(DS_LABEL[0..], 0, label); + const d : []const u8 = ds_bytes(); + const r : i64 = root(d); + DS_ROOT = r; + DS_N = 0; + DS_HAS_PERIOD = false; + DS_PERIOD = 0.0; + var lst : i64 = NO_SPAN; + if (fmt == 1) { lst = obj_get(d, r, "paths"); } else { lst = obj_get(d, r, "critical_paths"); } + fmax_entries(d, obj_get(d, r, "fmax"), fmt == 1, true); + if (fmt == 1) { + const pp : i64 = obj_get(d, r, "period_ps"); + if (jv_truthy(d, pp)) { + DS_HAS_PERIOD = true; + DS_PERIOD = numv(d, pp); + } + } + // the single fmax constraint, when there is exactly one clock + var one : i64 = NO_SPAN; + if (FM_N == 1) { one = FM_C[0]; } + var any_period : bool = false; + var min_period : f64 = 0.0; + if (kind(d, lst) == J_ARRAY) { + const hi : usize = span_hi(lst); + var p : usize = skip_ws(d, span_lo(lst) + 1); + while (p < hi and d[p] != 93 and DS_N < W_MAX_PATHS) { + const e : usize = value_end(d, p); + const sp : i64 = jspan(p, e); + const i : u32 = DS_N; + PSP[i as usize] = sp; + var total : f64 = 0.0; + var rout : f64 = 0.0; + const segs : i64 = segs_of(d, sp); + if (kind(d, segs) == J_ARRAY) { + const sh : usize = span_hi(segs); + var q : usize = skip_ws(d, span_lo(segs) + 1); + while (q < sh and d[q] != 93) { + const qe : usize = value_end(d, q); + const g : i64 = jspan(q, qe); + const ps : f64 = seg_ps(d, g); + total = total + ps; + if (seg_is_routing(d, g)) { rout = rout + ps; } + q = skip_ws(d, qe); + if (q < sh and d[q] == 44) { q = skip_ws(d, q + 1); } + } + } + TOT[i as usize] = total; + ROU[i as usize] = rout; + SLN[i as usize] = 1; + SLK[i as usize] = 0.0; + if (fmt == 1) { + const sl : i64 = obj_get(d, sp, "slack"); + if (present(d, sl)) { + SLN[i as usize] = 0; + SLK[i as usize] = numv(d, sl); + } + } else { + // c = cons[cp.to] ?? cons[cp.from] ?? one; period = c ? Math.round(1e6 / c) : null + var c : i64 = fm_constraint_of(d, obj_get(d, sp, "to")); + if (!present(d, c)) { c = fm_constraint_of(d, obj_get(d, sp, "from")); } + if (!present(d, c)) { c = one; } + if (jv_truthy(d, c)) { + const period : f64 = js_round(1000000.0 / numv(d, c)); + if (period == period and period != 0.0) { + if (!any_period or period < min_period) { min_period = period; } + any_period = true; + SLN[i as usize] = 0; + SLK[i as usize] = period - total; + } + } + } + DS_N = DS_N + 1; + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + } + if (fmt == 2 and any_period) { + DS_HAS_PERIOD = true; + DS_PERIOD = min_period; + } + sort_paths(DS_N); + return 0; +} + +/// The path at row k (sorted order) of the data. +fn row_path(k: u32) -> u32 { + return ORD[k as usize]; +} + +/// p.slack !== null && p.slack < 0 for the path at file index i. +fn path_fails(i: u32) -> bool { + return SLN[i as usize] == 0 and SLK[i as usize] < 0.0; +} + +/// The label of a demo: j.source?.top || '' as text; fills DS_LABEL through build_ds. +fn demo_label(tmp: []u8, d: []const u8, r: i64) -> u32 { + const t : i64 = obj_get(d, obj_get(d, r, "source"), "top"); + if (!jv_truthy(d, t)) { return 0; } + return str_of(tmp, d, t); +} + +// --- the libm memo and the text width memo ------------------------------------------------------------------- + +/// op 4 Math.log10(a), op 5 Math.pow(a, b): from the memo, else asked of the host (a miss makes the list wrong). +fn libm2(op: u32, a: f64, b: f64) -> f64 { + var i : u32 = 0; + while (i < LB_N) { + if ((LB_OP[i as usize] as u32) == op and LB_A[i as usize] == a and LB_B[i as usize] == b) { return LB_R[i as usize]; } + i = i + 1; + } + MISSING = true; + var j : u32 = 0; + while (j < RQ_N) { + if ((RQ_OP[j as usize] as u32) == op and RQ_A[j as usize] == a and RQ_B[j as usize] == b) { return 0.0; } + j = j + 1; + } + if (RQ_N < 64) { + RQ_OP[RQ_N as usize] = op as u8; + RQ_A[RQ_N as usize] = a; + RQ_B[RQ_N as usize] = b; + RQ_N = RQ_N + 1; + } + return 0.0; +} + +pub fn w_libm_need(dst: []f64) -> u32 { + if (RQ_N == 0) { return 0; } + var k : u32 = 0; + while (k < RQ_N and ((k * 3 + 2) as usize) < dst.len) { + dst[(k * 3) as usize] = RQ_OP[k as usize] as f64; + dst[(k * 3 + 1) as usize] = RQ_A[k as usize]; + dst[(k * 3 + 2) as usize] = RQ_B[k as usize]; + k = k + 1; + } + RQ_SENT = k; + return k; +} + +pub fn w_libm_give(src: []const f64, n: u32) -> u32 { + var k : u32 = 0; + while (k < n and k < RQ_SENT and (k as usize) < src.len) { + if (LB_N >= 64) { LB_N = 0; } + LB_OP[LB_N as usize] = RQ_OP[k as usize]; + LB_A[LB_N as usize] = RQ_A[k as usize]; + LB_B[LB_N as usize] = RQ_B[k as usize]; + LB_R[LB_N as usize] = src[k as usize]; + LB_N = LB_N + 1; + k = k + 1; + } + RQ_N = 0; + RQ_SENT = 0; + MISSING = false; + return 0; +} + +fn x_same(off: u32, len: u32, s: []const u8) -> bool { + if (len != (s.len as u32)) { return false; } + var k : u32 = 0; + while (k < len) { + if (X_POOL[(off + k) as usize] != s[k as usize]) { return false; } + k = k + 1; + } + return true; +} + +/// ctx.measureText(text).width under the font: from the memo, else asked of the host and read as 0 for now. +fn x_width(font: []const u8, text: []const u8) -> f64 { + var i : u32 = 0; + while (i < X_N) { + if (x_same(X_FO[i as usize], X_FL[i as usize], font) and x_same(X_TO[i as usize], X_TL[i as usize], text)) { + if (X_ST[i as usize] == 2) { return X_W[i as usize]; } + X_MISS = true; + return 0.0; + } + i = i + 1; + } + X_MISS = true; + if (X_N >= 256 or X_POOL_N + (font.len as u32) + (text.len as u32) > 16384) { + X_N = 0; + X_POOL_N = 0; + } + if ((font.len as u32) + (text.len as u32) > 16384) { return 0.0; } + const e : u32 = X_N; + X_FO[e as usize] = X_POOL_N; + X_FL[e as usize] = font.len as u32; + X_POOL_N = copy_into(X_POOL[0..], X_POOL_N, font); + X_TO[e as usize] = X_POOL_N; + X_TL[e as usize] = text.len as u32; + X_POOL_N = copy_into(X_POOL[0..], X_POOL_N, text); + X_ST[e as usize] = 0; + X_N = e + 1; + return 0.0; +} + +fn put_u32le(dst: []u8, at: u32, v: u32) -> u32 { + dst[at as usize] = (v % 256) as u8; + dst[(at + 1) as usize] = ((v / 256) % 256) as u8; + dst[(at + 2) as usize] = ((v / 65536) % 256) as u8; + dst[(at + 3) as usize] = ((v / 16777216) % 256) as u8; + return at + 4; +} + +/// Requests for the host: records of (u32 font length, u32 text length, font bytes, text bytes). The count. +pub fn w_text_need(dst: []u8) -> u32 { + var n : u32 = 0; + var c : u32 = 0; + var i : u32 = 0; + while (i < X_N) { + if (X_ST[i as usize] == 0) { + const need : u32 = 8 + X_FL[i as usize] + X_TL[i as usize]; + if ((n as usize) + (need as usize) > dst.len) { return c; } + n = put_u32le(dst, n, X_FL[i as usize]); + n = put_u32le(dst, n, X_TL[i as usize]); + var k : u32 = 0; + while (k < X_FL[i as usize]) { + dst[(n + k) as usize] = X_POOL[(X_FO[i as usize] + k) as usize]; + k = k + 1; + } + n = n + X_FL[i as usize]; + k = 0; + while (k < X_TL[i as usize]) { + dst[(n + k) as usize] = X_POOL[(X_TO[i as usize] + k) as usize]; + k = k + 1; + } + n = n + X_TL[i as usize]; + X_ST[i as usize] = 1; + c = c + 1; + } + i = i + 1; + } + return c; +} + +pub fn w_text_give(src: []const f64, n: u32) -> u32 { + var i : u32 = 0; + var k : u32 = 0; + while (i < X_N and k < n and (k as usize) < src.len) { + if (X_ST[i as usize] == 1) { + X_W[i as usize] = src[k as usize]; + X_ST[i as usize] = 2; + k = k + 1; + } + i = i + 1; + } + X_MISS = false; + return 0; +} + +// --- layout: tool.js's layout() and niceStep() ------------------------------------------------------------------ + +/// The step between axis labels, in ps: the first of 1, 2, 5, 10 times a power of ten that reaches range / (px / 70). +fn nice_step(range: f64, px: f64) -> f64 { + const target : f64 = range / js_max(2.0, px / 70.0); + const lg : f64 = libm2(4, target, 0.0); + const p : f64 = libm2(5, 10.0, js_floor(lg)); + if (1.0 * p >= target) { return 1.0 * p; } + if (2.0 * p >= target) { return 2.0 * p; } + if (5.0 * p >= target) { return 5.0 * p; } + if (10.0 * p >= target) { return 10.0 * p; } + return p * 10.0; +} + +fn dpr_now() -> f64 { + if (ENV_DPR == 0.0 or ENV_DPR != ENV_DPR) { return 1.0; } + return ENV_DPR; +} + +/// layout(): the row height, the canvas size and style, the box's max-height, the x range. +fn do_layout() -> u32 { + const n : f64 = DS_N as f64; + var max_h : f64 = 0.0; + if (ENV_EMBED != 0) { + max_h = js_max(120.0, ENV_IH - 265.0); + } else { + max_h = js_min(560.0, js_max(240.0, ENV_IH * 0.6)); + } + ROW_H = 0.0; + if (DS_N > 0) { ROW_H = js_max(2.0, js_min(7.0, js_floor((max_h - W_AXIS) / n))); } + CSS_W = ENV_BW; + if (CSS_W == 0.0 or CSS_W != CSS_W) { CSS_W = 600.0; } + const css_h : f64 = W_AXIS + js_max(1.0, n) * ROW_H + 2.0; + const dpr : f64 = dpr_now(); + CV_MAXH = js_round(max_h) as u64; + CV_W = js_round(CSS_W * dpr) as u64; + CV_H = js_round(css_h * dpr) as u64; + CV_SH = css_h as u64; + CV_SET = true; + var worst : f64 = 1.0; + if (DS_N > 0) { + worst = TOT[0]; + var i : u32 = 1; + while (i < DS_N) { + worst = js_max(worst, TOT[i as usize]); + i = i + 1; + } + } + if (FULL and DS_HAS_PERIOD) { + X_MAX = js_max(DS_PERIOD, worst) * 1.02; + } else { + X_MAX = worst * 1.06; + } + return 0; +} + +// --- the binary draw list ------------------------------------------------------------------------------------- + +fn bw_b(o: []u8, n: u32, b: u8) -> u32 { + if (n == OVER or (n as usize) >= o.len) { return OVER; } + o[n as usize] = b; + return n + 1; +} + +fn f64_bits(x: f64) -> u64 { + if (x == 0.0) { + if ((1.0 / x) < 0.0) { return 9223372036854775808; } + return 0; + } + var sign : u64 = 0; + var ax : f64 = x; + if (x < 0.0) { + sign = 9223372036854775808; + ax = 0.0 - x; + } + const m : u64 = jn_mant(ax); + const e : i32 = jn_expo(ax); + const be : u64 = ((e + 52 + 1023) as u64); + return sign + be * 4503599627370496 + (m - 4503599627370496); +} + +fn bw_f(o: []u8, n0: u32, x: f64) -> u32 { + var bits : u64 = f64_bits(x); + var n : u32 = n0; + var k : u32 = 0; + while (k < 8) { + n = bw_b(o, n, (bits % 256) as u8); + bits = bits / 256; + k = k + 1; + } + return n; +} + +fn bw_s(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = bw_b(o, n0, (s.len % 256) as u8); + n = bw_b(o, n, ((s.len / 256) % 256) as u8); + var k : usize = 0; + while (k < s.len) { + n = bw_b(o, n, s[k]); + k = k + 1; + } + return n; +} + +fn bw_set_s(o: []u8, n0: u32, op: u8, v: []const u8) -> u32 { + const n : u32 = bw_b(o, n0, op); + return bw_s(o, n, v); +} + +fn bw_set_f(o: []u8, n0: u32, op: u8, v: f64) -> u32 { + const n : u32 = bw_b(o, n0, op); + return bw_f(o, n, v); +} + +fn bw_rect(o: []u8, n0: u32, op: u8, a: f64, b: f64, c: f64, d: f64) -> u32 { + var n : u32 = bw_b(o, n0, op); + n = bw_f(o, n, a); + n = bw_f(o, n, b); + n = bw_f(o, n, c); + return bw_f(o, n, d); +} + +fn bw_text(o: []u8, n0: u32, s: []const u8, x: f64, y: f64) -> u32 { + var n : u32 = bw_b(o, n0, 13); + n = bw_s(o, n, s); + n = bw_f(o, n, x); + return bw_f(o, n, y); +} + +/// The font `10px ${getComputedStyle(root).getPropertyValue('--w-mono') || 'monospace'}` into f: the variable stands +/// for itself between byte 4 and byte 6, its fallback after byte 5 (the host does not trim the value). +fn font_into(f: []u8) -> u32 { + var n : u32 = copy_into(f, 0, "10px "); + f[n as usize] = 4; + n = copy_into(f, n + 1, W_MONO_VAR); + f[n as usize] = 5; + n = copy_into(f, n + 1, W_MONO_FB); + f[n as usize] = 6; + return n + 1; +} + +fn x_of(ps: f64) -> f64 { + return (ps / X_MAX) * (CSS_W - 2.0) + 1.0; +} + +/// The waterfall: what draw() paints on the canvas, call for call. +pub fn w_bdraw(o: []u8) -> u32 { + if (!RENDERED or DS_N == 0 or CUR == 0 or !CV_SET) { return 0; } + const d : []const u8 = ds_bytes(); + const dpr : f64 = dpr_now(); + const css_h : f64 = (CV_H as f64) / dpr; + var n : u32 = bw_b(o, 0, 200); + n = bw_b(o, n, 0); + n = bw_b(o, n, 12); + n = bw_f(o, n, dpr); + n = bw_f(o, n, 0.0); + n = bw_f(o, n, 0.0); + n = bw_f(o, n, dpr); + n = bw_f(o, n, 0.0); + n = bw_f(o, n, 0.0); + n = bw_set_s(o, n, 32, "#000"); + n = bw_rect(o, n, 10, 0.0, 0.0, CSS_W, css_h); + // axis + MISSING = false; + const step : f64 = nice_step(X_MAX, CSS_W); + if (MISSING) { return 0; } + var font : [160]u8 = [0; 160]; + const fl : u32 = font_into(font[0..]); + n = bw_b(o, n, 35); + n = bw_s(o, n, font[0..(fl as usize)]); + n = bw_set_s(o, n, 36, "middle"); + var t : f64 = 0.0; + var guard : u32 = 0; + while (t <= X_MAX and step > 0.0 and guard < 100000) { + const px : f64 = js_round(x_of(t)) + 0.5; + n = bw_set_s(o, n, 32, "rgba(0,255,136,0.12)"); + n = bw_rect(o, n, 10, px, W_AXIS, 1.0, css_h - W_AXIS); + n = bw_set_s(o, n, 32, "#8fa9a0"); + var lb : [64]u8 = [0; 64]; + var ll : u32 = put_fixed_num(lb[0..], 0, t / (K_PS_PER_NS as f64), 3); + var lab : [160]u8 = [0; 160]; + var vb : [96]u8 = [0; 96]; + var vn : u32 = jv_vput_str(vb[0..], 0, "0", lb[0..(ll as usize)]); + var al : u32 = jv_fill_raw(lab[0..], 0, SAY_AXIS, vb[0..], vn, true); + if (al == OVER) { al = 0; } + const w : f64 = x_width(font[0..(fl as usize)], lab[0..(al as usize)]); + if (px + 2.0 + w < CSS_W) { n = bw_text(o, n, lab[0..(al as usize)], px + 2.0, W_AXIS / 2.0); } + t = t + step; + guard = guard + 1; + } + // rows + var gap : f64 = 0.0; + if (ROW_H >= 4.0) { gap = 1.0; } + var row : u32 = 0; + while (row < DS_N) { + const pi : u32 = ORD[row as usize]; + const y : f64 = W_AXIS + (row as f64) * ROW_H; + const hgt : f64 = ROW_H - gap; + if (DS_HAS_PERIOD and TOT[pi as usize] < DS_PERIOD) { + n = bw_set_s(o, n, 32, K_SLACK_COLOR); + n = bw_rect(o, n, 10, x_of(TOT[pi as usize]), y, x_of(DS_PERIOD) - x_of(TOT[pi as usize]), hgt); + } + var tt : f64 = 0.0; + const segs : i64 = segs_of(d, PSP[pi as usize]); + if (kind(d, segs) == J_ARRAY) { + const sh : usize = span_hi(segs); + var q : usize = skip_ws(d, span_lo(segs) + 1); + var k : u32 = 0; + while (q < sh and d[q] != 93) { + const qe : usize = value_end(d, q); + const g : i64 = jspan(q, qe); + var col : [16]u8 = [0; 16]; + const cl : u32 = put_color(col[0..], 0, d, seg_kind(d, g)); + n = bw_set_s(o, n, 32, col[0..(cl as usize)]); + if (k % 2 == 1) { n = bw_set_f(o, n, 38, 0.78); } else { n = bw_set_f(o, n, 38, 1.0); } + const ps : f64 = seg_ps(d, g); + n = bw_rect(o, n, 10, x_of(tt), y, js_max(0.6, x_of(tt + ps) - x_of(tt)), hgt); + tt = tt + ps; + k = k + 1; + q = skip_ws(d, qe); + if (q < sh and d[q] == 44) { q = skip_ws(d, q + 1); } + } + } + n = bw_set_f(o, n, 38, 1.0); + if (path_fails(pi)) { + var per : f64 = 0.0; + if (DS_HAS_PERIOD) { per = DS_PERIOD; } + n = bw_set_s(o, n, 32, K_FAIL_COLOR); + n = bw_rect(o, n, 10, x_of(per), y, x_of(TOT[pi as usize]) - x_of(per), hgt); + n = bw_rect(o, n, 10, 0.0, y, 3.0, hgt); + } + row = row + 1; + } + // period line + if (DS_HAS_PERIOD and DS_PERIOD <= X_MAX) { + n = bw_set_s(o, n, 32, K_FAIL_COLOR); + n = bw_rect(o, n, 10, js_round(x_of(DS_PERIOD)) - 1.0, 0.0, 2.0, css_h); + } + // the picked row + const py : f64 = W_AXIS + (PICK as f64) * ROW_H; + n = bw_set_s(o, n, 33, "#e8fff7"); + n = bw_set_f(o, n, 34, 1.0); + n = bw_rect(o, n, 11, 0.5, py - 1.5, CSS_W - 1.0, ROW_H + 2.0); + if (n == OVER) { return 0; } + return n; +} + +// --- text pieces: tool.js's fmt() and the detail of one path ----------------------------------------------------- + +fn pool_dec(vb: []u8, n0: u32, key: []const u8, v: u64) -> u32 { + var tmp : [32]u8 = [0; 32]; + const l : u32 = dec_to(tmp[0..], v); + return jv_vput_str(vb, n0, key, tmp[0..(l as usize)]); +} + +/// `${g.from.cell}.${g.from.port}` (report) or `${str(f)}.${fp}` (own) as raw bytes; the segment's from (cell 2, 3), +/// to (cell 4, 5) or a report segment's "from" / "to" object. +fn seg_end_raw(tmp: []u8, d: []const u8, g: i64, which: u32) -> u32 { + if (DS_FMT == 1) { + const strs : i64 = obj_get(d, DS_ROOT, "strings"); + var base : u32 = 2; + if (which == 1) { base = 4; } + var n : u32 = put_str_idx(tmp, 0, d, strs, arr_at(d, g, base)); + n = put(tmp, n, 46); + return jv_str(tmp, n, d, arr_at(d, g, base + 1)); + } + var key : []const u8 = "from"; + if (which == 1) { key = "to"; } + const e : i64 = obj_get(d, g, key); + if (!jv_truthy(d, e)) { return 0; } + var n : u32 = jv_str(tmp, 0, d, obj_get(d, e, "cell")); + n = put(tmp, n, 46); + return jv_str(tmp, n, d, obj_get(d, e, "port")); +} + +fn seg_net_raw(tmp: []u8, d: []const u8, g: i64) -> u32 { + if (DS_FMT == 1) { + return put_str_idx(tmp, 0, d, obj_get(d, DS_ROOT, "strings"), arr_at(d, g, 6)); + } + const nt : i64 = obj_get(d, g, "net"); + if (!jv_truthy(d, nt)) { return 0; } + return jv_str(tmp, 0, d, nt); +} + +/// The last segment of a path, or NO_SPAN. +fn last_seg(d: []const u8, sp: i64) -> i64 { + const segs : i64 = segs_of(d, sp); + if (kind(d, segs) != J_ARRAY) { return NO_SPAN; } + const c : u32 = arr_len(d, segs); + if (c == 0) { return NO_SPAN; } + return arr_at(d, segs, c - 1); +} + +/// `${cp.from} -> ${cp.to}` of a report path as raw bytes. +fn clocks_raw(tmp: []u8, d: []const u8, sp: i64) -> u32 { + var n : u32 = jv_str(tmp, 0, d, obj_get(d, sp, "from")); + n = copy_into(tmp, n, " -> "); + return jv_str(tmp, n, d, obj_get(d, sp, "to")); +} + +/// The path's `to` as raw bytes: own `${str(p.to)}.${p.pin}`, report the last segment's to, else its clocks. +fn path_to_raw(tmp: []u8, d: []const u8, sp: i64) -> u32 { + if (DS_FMT == 1) { + var n : u32 = put_str_idx(tmp, 0, d, obj_get(d, DS_ROOT, "strings"), obj_get(d, sp, "to")); + n = put(tmp, n, 46); + return jv_str(tmp, n, d, obj_get(d, sp, "pin")); + } + const last : i64 = last_seg(d, sp); + if (last == NO_SPAN) { return clocks_raw(tmp, d, sp); } + return seg_end_raw(tmp, d, last, 1); +} + +/// showDetail(): the title, the line and the table of the picked path, into o. +fn put_detail(o: []u8, n0: u32) -> u32 { + const d : []const u8 = ds_bytes(); + const pi : u32 = ORD[PICK as usize]; + const sp : i64 = PSP[pi as usize]; + const failed : bool = path_fails(pi); + var n : u32 = raw(o, n0, "

    "); + var vb : [8192]u8 = [0; 8192]; + var vn : u32 = pool_dec(vb[0..], 0, "0", (PICK + 1) as u64); + vn = pool_dec(vb[0..], vn, "1", DS_N as u64); + var xt : [4200]u8 = [0; 4200]; + var xl : u32 = path_to_raw(xt[0..], d, sp); + if (DS_FMT == 2) { + xl = copy_into(xt[0..], xl, " ("); + var ck : [2200]u8 = [0; 2200]; + const cl : u32 = clocks_raw(ck[0..], d, sp); + xl = copy_into(xt[0..], xl, ck[0..(cl as usize)]); + xl = copy_into(xt[0..], xl, ")"); + } + vn = jv_vput_str(vb[0..], vn, "2", xt[0..(xl as usize)]); + if (vn == OVER) { return OVER; } + n = jv_fill(o, n, SAY_PATH_TITLE, vb[0..], vn, true); + n = raw(o, n, "

    "); + var v2 : [8192]u8 = [0; 8192]; + var tn : u32 = 0; + var t0 : [64]u8 = [0; 64]; + var t1 : [64]u8 = [0; 64]; + var t2 : [64]u8 = [0; 64]; + var t3 : [64]u8 = [0; 64]; + const l0 : u32 = put_ns(t0[0..], 0, TOT[pi as usize]); + const l1 : u32 = put_ns(t1[0..], 0, TOT[pi as usize] - ROU[pi as usize]); + const l2 : u32 = put_ns(t2[0..], 0, ROU[pi as usize]); + tn = jv_vput_str(v2[0..], 0, "0", t0[0..(l0 as usize)]); + tn = jv_vput_str(v2[0..], tn, "1", t1[0..(l1 as usize)]); + tn = jv_vput_str(v2[0..], tn, "2", t2[0..(l2 as usize)]); + if (SLN[pi as usize] == 0) { + const l3 : u32 = put_signed(t3[0..], 0, SLK[pi as usize], false); + tn = jv_vput_str(v2[0..], tn, "3", t3[0..(l3 as usize)]); + n = jv_fill(o, n, SAY_PATH_LINE, v2[0..], tn, true); + } else { + n = jv_fill(o, n, SAY_PATH_LINE_NO_PERIOD, v2[0..], tn, true); + } + n = raw(o, n, "

    "); + var h : usize = 0; + while (h < 5) { + n = raw(o, n, ""); + h = h + 1; + } + n = raw(o, n, ""); + const segs : i64 = segs_of(d, sp); + if (kind(d, segs) == J_ARRAY) { + const sh : usize = span_hi(segs); + var q : usize = skip_ws(d, span_lo(segs) + 1); + while (q < sh and d[q] != 93) { + const qe : usize = value_end(d, q); + const g : i64 = jspan(q, qe); + n = raw(o, n, ""); + q = skip_ws(d, qe); + if (q < sh and d[q] == 44) { q = skip_ws(d, q + 1); } + } + } + return raw(o, n, "
    "); + n = text(o, n, SAY_SEG_HEAD[h]); + n = raw(o, n, "
    "); + if (DS_FMT == 1) { + n = put_kid(o, n, d, seg_kind(d, g)); + } else { + if (jv_truthy(d, seg_kind(d, g))) { n = jv_text(o, n, d, seg_kind(d, g)); } + } + n = raw(o, n, ""); + n = put_ns(o, n, seg_ps(d, g)); + n = raw(o, n, ""); + var cell : [4200]u8 = [0; 4200]; + var cn : u32 = seg_end_raw(cell[0..], d, g, 0); + n = text(o, n, cell[0..(cn as usize)]); + n = raw(o, n, ""); + cn = seg_end_raw(cell[0..], d, g, 1); + n = text(o, n, cell[0..(cn as usize)]); + n = raw(o, n, ""); + cn = seg_net_raw(cell[0..], d, g); + n = text(o, n, cell[0..(cn as usize)]); + n = raw(o, n, "
    "); +} + +/// The fmax table into o. +fn put_fmax(o: []u8, n0: u32) -> u32 { + const d : []const u8 = ds_bytes(); + var n : u32 = raw(o, n0, ""); + var h : usize = 0; + while (h < 4) { + n = raw(o, n, ""); + h = h + 1; + } + n = raw(o, n, ""); + var i : u32 = 0; + while (i < FM_N) { + const a : i64 = FM_A[i as usize]; + const c : i64 = FM_C[i as usize]; + const cpos : bool = numv(d, c) > 0.0; + const good : bool = !cpos or numv(d, a) >= numv(d, c); + n = raw(o, n, ""); + i = i + 1; + } + return raw(o, n, "
    "); + n = text(o, n, SAY_FMAX_HEAD[h]); + n = raw(o, n, "
    "); + var kt : [1100]u8 = [0; 1100]; + const kl : u32 = fm_key_raw(kt[0..], d, i); + n = text(o, n, kt[0..(kl as usize)]); + n = raw(o, n, ""); + if (!present(d, a)) { n = put(o, n, 45); } else { n = js_to_fixed(o, n, numv(d, a), 2); } + n = raw(o, n, ""); + if (!present(d, c)) { n = put(o, n, 45); } else { n = js_to_fixed(o, n, numv(d, c), 2); } + if (good) { n = raw(o, n, ""); } else { n = raw(o, n, ""); } + if (cpos) { + if (good) { n = text(o, n, SAY_PASS); } else { n = text(o, n, SAY_FAIL); } + } + n = raw(o, n, "
    "); +} + +/// UTF-16 length of the decoded string at sp. +fn utf16_len(d: []const u8, sp: i64) -> u32 { + const l : u32 = str_len(d, sp); + var n : u32 = 0; + var i : u32 = 0; + while (i < l) { + const b : u8 = str_byte(d, sp, i); + if (b < 128 or b >= 192) { + n = n + 1; + if (b >= 240) { n = n + 1; } + } + i = i + 1; + } + return n; +} + +/// The check line of a timing.json into o (escaped), as fmt(SAY_CHECK_LINE, ...). +fn put_check(o: []u8, n0: u32) -> u32 { + const d : []const u8 = ds_bytes(); + const c : i64 = obj_get(d, DS_ROOT, "check"); + var vb : [8192]u8 = [0; 8192]; + var vn : u32 = 0; + var k : u32 = 0; + while (k < 3) { + var key : []const u8 = "nextpnr_critical_total_ns"; + if (k == 1) { key = "nextpnr_critical_endpoint"; } + if (k == 2) { key = "sdf_worst_total_ps"; } + const v : i64 = obj_get(d, c, key); + var kb : [2]u8 = [0; 2]; + kb[0] = 48 + (k as u8); + if (present(d, v)) { vn = jv_vput_val(vb[0..], vn, kb[0..1], d, v); } else { vn = jv_vput_str(vb[0..], vn, kb[0..1], ""); } + k = k + 1; + } + const h : i64 = obj_get(d, c, "nextpnr_histogram"); + var have_bins : bool = true; + var bins : u32 = 0; + if (jv_truthy(d, h)) { + const hk : u8 = kind(d, h); + if (hk == J_ARRAY) { + bins = arr_len(d, h); + } else { + if (hk == J_STRING) { bins = utf16_len(d, h); } else { have_bins = false; } + } + } + var b4 : [16]u8 = [0; 16]; + var bl : u32 = 0; + if (have_bins) { bl = dec_to(b4[0..], bins as u64); } + if (jv_truthy(d, obj_get(d, c, "histogram_bins_equal"))) { + vn = jv_vput_str(vb[0..], vn, "3", b4[0..(bl as usize)]); + } else { + vn = jv_vput_str(vb[0..], vn, "3", "0"); + } + vn = jv_vput_str(vb[0..], vn, "4", b4[0..(bl as usize)]); + if (vn == OVER) { return OVER; } + return jv_fill(o, n0, SAY_CHECK_LINE, vb[0..], vn, true); +} + +/// The "how this was made" body into o: the summary, the joined fields and the commands. +fn put_how(o: []u8, n0: u32) -> u32 { + const d : []const u8 = ds_bytes(); + const src : i64 = obj_get(d, DS_ROOT, "source"); + var n : u32 = raw(o, n0, ""); + n = text(o, n, SAY_COMMANDS); + n = raw(o, n, "

    "); + var first : bool = true; + var i : u32 = 0; + while (i < 5) { + var key : []const u8 = "design"; + if (i == 1) { key = "part"; } + if (i == 2) { key = "nextpnr"; } + if (i == 3) { key = "yosys"; } + if (i == 4) { key = "when"; } + const v : i64 = obj_get(d, src, key); + if (jv_truthy(d, v)) { + if (!first) { n = raw(o, n, " / "); } + first = false; + n = jv_text(o, n, d, v); + } + i = i + 1; + } + n = raw(o, n, "

      "); + const cm : i64 = obj_get(d, src, "commands"); + if (kind(d, cm) == J_ARRAY) { + const hi : usize = span_hi(cm); + var p : usize = skip_ws(d, span_lo(cm) + 1); + while (p < hi and d[p] != 93) { + const e : usize = value_end(d, p); + n = raw(o, n, "
    • "); + n = put_kid(o, n, d, jspan(p, e)); + n = raw(o, n, "
    • "); + p = skip_ws(d, e); + if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); } + } + } + return raw(o, n, "
    "); +} + +// --- state: tool.js's use(), render(), step() and the events -------------------------------------------------- + +pub var DEMO_HID : bool = true; +pub var CHK : [16384]u8 = [0; 16384]; +pub var CHK_LEN : u32 = 0; +pub var HOW : [262144]u8 = [0; 262144]; +pub var HOW_LEN : u32 = 0; + +fn refresh_detail() -> u32 { + if (DS_N == 0 or CUR == 0) { return 0; } + const l : u32 = put_detail(DET[0..], 0); + DET_LEN = 0; + if (l != OVER) { DET_LEN = l; } + return 0; +} + +fn has_check() -> bool { + if (CUR == 0 or DS_FMT != 1) { return false; } + const d : []const u8 = ds_bytes(); + return jv_truthy(d, obj_get(d, DS_ROOT, "check")); +} + +fn has_source() -> bool { + if (CUR == 0 or DS_FMT != 1) { return false; } + const d : []const u8 = ds_bytes(); + return jv_truthy(d, obj_get(d, DS_ROOT, "source")); +} + +/// render(): everything that depends on the data in use. The parts it leaves alone when there is no path (the +/// detail, the canvas size) keep their old look; the check line and the commands keep it when the data has none. +fn do_render() -> u32 { + if (CUR == 0) { return 0; } + ST = 7; + RENDERED = true; + ST_FAIL = DS_N > 0 and path_fails(ORD[0]); + if (has_check()) { + const l : u32 = put_check(CHK[0..], 0); + CHK_LEN = 0; + if (l != OVER) { CHK_LEN = l; } + } + if (has_source()) { + const l : u32 = put_how(HOW[0..], 0); + HOW_LEN = 0; + if (l != OVER) { HOW_LEN = l; } + } + if (DS_N > 0) { + do_layout(); + refresh_detail(); + } + return 0; +} + +/// use(d): the data in use is the buffer CUR names, built by build_ds. +fn use_data(is_demo: bool) -> u32 { + PICK = 0; + DEMO_HID = is_demo or !DEMO_OK; + EF[0] = 120; + EF_N = 1; + return do_render(); +} + +fn step(k: f64) -> u32 { + if (CUR == 0 or DS_N == 0) { return 0; } + var next : f64 = js_max(0.0, js_min((DS_N as f64) - 1.0, (PICK as f64) + k)); + PICK = next as u32; + refresh_detail(); + EF[0] = 121; + EF_N = 1; + return 0; +} + +pub fn w_init(h: []const u8) -> u32 { + CUR = 0; + DEMO_OK = false; + DEMO_LEN = 0; + FA_LEN = 0; + FB_LEN = 0; + FULL = true; + PICK = 0; + ST = 0; + ST_FAIL = false; + RENDERED = false; + OVER_DROP = false; + CLR_FILE = false; + EF_N = 0; + LAST_W = 0.0; + CV_SET = false; + ROW_H = 3.0; + CSS_W = 0.0; + X_MAX = 1.0; + DET_LEN = 0; + CHK_LEN = 0; + HOW_LEN = 0; + DEMO_HID = true; + DS_FMT = 0; + DS_N = 0; + DS_HAS_PERIOD = false; + FM_N = 0; + return 0; +} + +pub fn w_view(width: f64, dpr: f64, embed: u32, ih: f64, bw: f64) -> u32 { + ENV_W = width; + ENV_DPR = dpr; + ENV_EMBED = embed; + ENV_IH = ih; + ENV_BW = bw; + return 0; +} + +pub fn w_measure_sel(dst: []u8) -> u32 { + return copy_into(dst, 0, W_SEL); +} + +pub fn w_scroll_sel(dst: []u8) -> u32 { + return copy_into(dst, 0, W_SEL); +} + +pub fn w_canvas_keep() -> u32 { + return 1; +} + +pub fn w_drop_root() -> u32 { + return 1; +} + +pub fn w_draw_cap() -> u32 { + return W_DRAW_CAP; +} + +pub fn w_file_cap() -> u32 { + return W_FILE_CAP; +} + +/// The timing.json of the page, fetched at load: fromOwn, then agrees(), then use(); anything else is a status. +pub fn w_data(i: u32, s: []const u8) -> u32 { + if (s.len > (W_DEMO_CAP as usize)) { + ST = 1; + return 0; + } + var k : usize = 0; + while (k < s.len) { + DEMO[k] = s[k]; + k = k + 1; + } + DEMO_LEN = s.len as u32; + const d : []const u8 = DEMO[0..(DEMO_LEN as usize)]; + if (!jv_valid(d)) { + ST = 1; + return 0; + } + const r : i64 = root(d); + if (kind(d, r) == J_NULL) { + ST = 1; + return 0; + } + if (own_throws(d, r)) { + ST = 1; + return 0; + } + const prev : u32 = CUR; + const prev_fmt : u32 = DS_FMT; + var keep : [1100]u8 = [0; 1100]; + const keep_len : u32 = copy_into(keep[0..], 0, DS_LABEL[0..(DS_LABEL_LEN as usize)]); + CUR = 1; + var lb : [1100]u8 = [0; 1100]; + const ll : u32 = demo_label(lb[0..], d, r); + build_ds(1, lb[0..(ll as usize)]); + var agrees : bool = false; + if (DS_N > 0) { + const p : u32 = ORD[0]; + agrees = DS_HAS_PERIOD and DS_PERIOD == (K_DEMO_PERIOD_PS as f64) and DS_N == (K_DEMO_ENDPOINTS as u32) and TOT[p as usize] == (K_DEMO_WORST_TOTAL_PS as f64) and ROU[p as usize] == (K_DEMO_WORST_ROUTING_PS as f64) and SLN[p as usize] == 0 and SLK[p as usize] == (K_DEMO_WORST_SLACK_PS as f64); + } + if (!agrees) { + CUR = prev; + DS_N = 0; + if (prev != 0) { build_ds(prev_fmt, keep[0..(keep_len as usize)]); } + ST = 2; + return 0; + } + DEMO_OK = true; + use_data(true); + return 0; +} + +/// The demo's data again (the buffer is intact; the tables are rebuilt). +fn build_demo() -> u32 { + const d : []const u8 = DEMO[0..(DEMO_LEN as usize)]; + var lb : [1100]u8 = [0; 1100]; + const ll : u32 = demo_label(lb[0..], d, root(d)); + return build_ds(1, lb[0..(ll as usize)]); +} + +pub fn w_data_failed(i: u32) -> u32 { + ST = 1; + return 0; +} + +pub fn w_click(i: u32) -> u32 { + EF_N = 0; + if (i == 0) { + FULL = true; + if (CUR != 0) { + do_render(); + } + return 0; + } + if (i == 1) { + FULL = false; + if (CUR != 0) { + do_render(); + } + return 0; + } + if (i == 2) { return step(-1.0); } + if (i == 3) { return step(1.0); } + if (i == 4) { + if (DEMO_OK) { + CUR = 1; + build_demo(); + use_data(true); + } + } + return 0; +} + +fn word_is(key: []const u8, want: []const u8) -> bool { + if (key.len != want.len) { return false; } + var i : usize = 0; + while (i < key.len) { + if (key[i] != want[i]) { return false; } + i = i + 1; + } + return true; +} + +pub fn w_key(key: []const u8) -> u32 { + EF_N = 0; + const n : f64 = (DS_N as f64); + var cnt : f64 = 0.0; + if (CUR != 0) { cnt = n; } + if (word_is(key, "ArrowUp")) { + step(-1.0); + return 1; + } + if (word_is(key, "ArrowDown")) { + step(1.0); + return 1; + } + if (word_is(key, "PageUp")) { + step(-10.0); + return 1; + } + if (word_is(key, "PageDown")) { + step(10.0); + return 1; + } + if (word_is(key, "Home")) { + step(0.0 - cnt); + return 1; + } + if (word_is(key, "End")) { + step(cnt); + return 1; + } + return 0; +} + +pub fn w_cpointer(kind_of: u32, id: i64, x: f64, y: f64, mouse: u32) -> u32 { + EF_N = 0; + if (kind_of != 0) { return 0; } + if (CUR == 0 or ROW_H == 0.0) { return 0; } + const i : f64 = js_floor((y - W_AXIS) / ROW_H); + if (i < 0.0 or i >= (DS_N as f64)) { return 0; } + PICK = i as u32; + refresh_detail(); + return 1; +} + +pub fn w_box(w: f64, h: f64) -> u32 { + EF_N = 0; + if (CUR == 0 or DS_N == 0 or w == LAST_W) { return 0; } + LAST_W = w; + do_layout(); + return 1; +} + +pub fn w_drag(kind_of: u32) -> u32 { + EF_N = 0; + const was : bool = OVER_DROP; + OVER_DROP = kind_of == 0; + if (was != OVER_DROP) { return 1; } + return 0; +} + +pub fn w_value(i: u32, dst: []u8) -> u32 { + if (i == 0 and CLR_FILE) { + CLR_FILE = false; + return 0; + } + return 4294967295; +} + +/// takeFile(file): the text the host decoded. The checks of tool.js come in its order; a throw leaves "Reading". +pub fn w_file(name: []const u8, size: f64, data: []const u8, st: u32) -> u32 { + EF_N = 0; + CLR_FILE = true; + ST_NAME_LEN = copy_into(ST_NAME[0..], 0, name); + if (size > (W_FILE_CAP as f64)) { + ST = 6; + return 0; + } + ST = 3; + if (st != 0) { + ST = 4; + return 0; + } + if (data.len > (W_FILE_CAP as usize)) { + ST = 6; + return 0; + } + var which : u32 = 2; + if (CUR == 2) { which = 3; } + var k : usize = 0; + while (k < data.len) { + if (which == 2) { FA[k] = data[k]; } else { FB[k] = data[k]; } + k = k + 1; + } + if (which == 2) { FA_LEN = data.len as u32; } else { FB_LEN = data.len as u32; } + var d : []const u8 = FA[0..(FA_LEN as usize)]; + if (which == 3) { d = FB[0..(FB_LEN as usize)]; } + if (!jv_valid(d)) { + ST = 4; + return 0; + } + const r : i64 = root(d); + var fmt : u32 = 0; + if (jv_is(d, obj_get(d, r, "t27"), K_SCHEMA) and kind(d, obj_get(d, r, "paths")) == J_ARRAY) { + fmt = 1; + } else { + const cps : i64 = obj_get(d, r, "critical_paths"); + const fx : i64 = obj_get(d, r, "fmax"); + if (kind(d, cps) == J_ARRAY) { fmt = 2; } + if (fmt == 0 and jv_truthy(d, fx) and (kind(d, fx) == J_OBJECT or kind(d, fx) == J_ARRAY)) { fmt = 2; } + } + if (fmt == 0) { + ST = 5; + return 0; + } + if (fmt == 1 and own_throws(d, r)) { return 0; } + if (fmt == 2 and report_throws(d, r)) { return 0; } + CUR = which; + build_ds(fmt, name); + use_data(false); + return 0; +} + +pub fn w_effect(k: u32, dst: []u8) -> u32 { + if (k >= EF_N) { + EF_N = 0; + return 0; + } + var n : u32 = 0; + if (EF[k as usize] == 120) { + n = put(dst, 0, 120); + n = raw(dst, n, W_SEL); + } else { + n = put(dst, 0, 121); + n = raw(dst, n, W_SEL); + n = put(dst, n, 10); + n = js_str_num(dst, n, W_AXIS + (PICK as f64) * ROW_H); + n = put(dst, n, 10); + n = js_str_num(dst, n, ROW_H); + } + if (n == OVER) { return 0; } + return n; +} + +// --- the page -------------------------------------------------------------------------------------------------- + +fn hid(o: []u8, n: u32, h: bool) -> u32 { + if (h) { return raw(o, n, " hidden=\"\""); } + return n; +} + +/// The words of the status line: a message, or the summary of the data in use. +fn put_status(o: []u8, n0: u32) -> u32 { + if (ST == 0) { return text(o, n0, SAY_LOADING); } + if (ST == 1) { return text(o, n0, SAY_ERR_DEMO); } + if (ST == 2) { return text(o, n0, SAY_ERR_DEMO_DIFF); } + var vb : [4096]u8 = [0; 4096]; + var vn : u32 = 0; + if (ST >= 3 and ST <= 6) { + vn = jv_vput_str(vb[0..], 0, "0", ST_NAME[0..(ST_NAME_LEN as usize)]); + if (ST == 3) { return jv_fill(o, n0, SAY_READING, vb[0..], vn, true); } + if (ST == 4) { return jv_fill(o, n0, SAY_ERR_JSON, vb[0..], vn, true); } + if (ST == 5) { return jv_fill(o, n0, SAY_ERR_SHAPE, vb[0..], vn, true); } + vn = pool_dec(vb[0..], vn, "1", W_FILE_MB as u64); + return jv_fill(o, n0, SAY_ERR_SIZE, vb[0..], vn, true); + } + // the summary + if (DS_N == 0) { return text(o, n0, DS_LABEL[0..(DS_LABEL_LEN as usize)]); } + const p : u32 = ORD[0]; + vn = jv_vput_str(vb[0..], 0, "0", DS_LABEL[0..(DS_LABEL_LEN as usize)]); + var cnt : [32]u8 = [0; 32]; + const cl : u32 = js_locale_num(cnt[0..], 0, DS_N as f64); + vn = jv_vput_str(vb[0..], vn, "1", cnt[0..(cl as usize)]); + var t2 : [96]u8 = [0; 96]; + if (DS_HAS_PERIOD) { + const l2 : u32 = put_signed(t2[0..], 0, SLK[p as usize], SLN[p as usize] != 0); + vn = jv_vput_str(vb[0..], vn, "2", t2[0..(l2 as usize)]); + var t3 : [64]u8 = [0; 64]; + const l3 : u32 = put_fixed_num(t3[0..], 0, 1000000.0 / DS_PERIOD, 2); + vn = jv_vput_str(vb[0..], vn, "3", t3[0..(l3 as usize)]); + if (vn == OVER) { return OVER; } + return jv_fill(o, n0, SAY_SUMMARY, vb[0..], vn, true); + } + const l2 : u32 = put_ns(t2[0..], 0, TOT[p as usize]); + vn = jv_vput_str(vb[0..], vn, "2", t2[0..(l2 as usize)]); + if (vn == OVER) { return OVER; } + return jv_fill(o, n0, SAY_SUMMARY_NO_PERIOD, vb[0..], vn, true); +} + +fn color_named(name: []const u8) -> []const u8 { + var i : usize = 0; + while (i < K_SEG_TYPES.len) { + if (word_is(K_SEG_TYPES[i], name)) { return K_SEG_COLORS[i]; } + i = i + 1; + } + return K_SEG_COLORS[2]; +} + +fn put_legend_item(o: []u8, n0: u32, style: []const u8, cls: []const u8, label: []const u8) -> u32 { + var n : u32 = raw(o, n0, "
  • 0) { + n = raw(o, n, " class=\""); + n = raw(o, n, cls); + n = raw(o, n, "\""); + } + n = attr(o, n, "style", style); + n = raw(o, n, ">"); + n = text(o, n, label); + return raw(o, n, "
  • "); +} + +pub fn w_render(dst: []u8) -> u32 { + const have : bool = RENDERED; + const nz : bool = DS_N > 0; + const no_paths : bool = have and !nz; + var n : u32 = raw(dst, 0, "

    "); + n = put_status(dst, n); + n = raw(dst, n, "

    "); + if (have) { n = put_fmax(dst, n); } + n = raw(dst, n, "

    "); + if (note_on) { + var vb : [2400]u8 = [0; 2400]; + const vn : u32 = jv_vput_str(vb[0..], 0, "0", DS_LABEL[0..(DS_LABEL_LEN as usize)]); + n = jv_fill(dst, n, SAY_NO_PATHS, vb[0..], vn, true); + } + n = raw(dst, n, "

    "); + n = text(dst, n, SAY_HINT); + n = raw(dst, n, "

      "); + var sty : [96]u8 = [0; 96]; + var sl : u32 = copy_into(sty[0..], 0, "background:"); + sl = copy_into(sty[0..], sl, color_named("logic")); + n = put_legend_item(dst, n, sty[0..(sl as usize)], "", SAY_LEGEND[0]); + sl = copy_into(sty[0..], 0, "background:"); + sl = copy_into(sty[0..], sl, color_named("routing")); + n = put_legend_item(dst, n, sty[0..(sl as usize)], "", SAY_LEGEND[1]); + sl = copy_into(sty[0..], 0, "background:"); + sl = copy_into(sty[0..], sl, color_named("setup")); + n = put_legend_item(dst, n, sty[0..(sl as usize)], "", SAY_LEGEND[2]); + sl = copy_into(sty[0..], 0, "background:"); + sl = copy_into(sty[0..], sl, K_SLACK_COLOR); + sl = copy_into(sty[0..], sl, ";border:1px solid "); + sl = copy_into(sty[0..], sl, W_LINE_FB); + n = put_legend_item(dst, n, sty[0..(sl as usize)], "", SAY_LEGEND[3]); + sl = copy_into(sty[0..], 0, "background:"); + sl = copy_into(sty[0..], sl, K_FAIL_COLOR); + n = put_legend_item(dst, n, sty[0..(sl as usize)], "sw-period", SAY_LEGEND[4]); + n = raw(dst, n, "
    "); + n = raw(dst, n, DET[0..(DET_LEN as usize)]); + n = raw(dst, n, "

    "); + n = raw(dst, n, CHK[0..(CHK_LEN as usize)]); + n = raw(dst, n, "

    "); + n = raw(dst, n, HOW[0..(HOW_LEN as usize)]); + n = raw(dst, n, "
    "); + if (n == OVER) { return 0; } + return n; +} + +pub fn same_text(a: []const u8, b: []const u8) -> bool { + if (a.len != b.len) { return false; } + var i : usize = 0; + while (i < a.len) { + if (a[i] != b[i]) { return false; } + i = i + 1; + } + return true; +} + +// --- Tests of the native page ----------------------------------------------------------------------------- + +pub const W_TEST_OWN : str = "{\"t27\":\"slack-waterfall/1\",\"period_ps\":20000,\"strings\":[\"a\",\"b\"],\"paths\":[{\"to\":1,\"pin\":\"D\",\"slack\":15233,\"segs\":[[\"clk-to-q\",100,0,\"CK\",0,\"Q\",-1],[\"routing\",270,0,\"Q\",1,\"A1\",1],[\"setup\",100,1,\"A1\",1,\"A1\",-1]]},{\"to\":0,\"pin\":\"Q\",\"slack\":-5,\"segs\":[[\"logic\",20005,0,\"A\",1,\"B\",-1]]}],\"fmax\":{\"clk\":{\"achieved\":209.78,\"constraint\":50}}}"; +pub const W_TEST_REPORT : str = "{\"fmax\":{\"clk\":{\"achieved\":100,\"constraint\":50}},\"critical_paths\":[{\"from\":\"clk\",\"to\":\"clk\",\"path\":[{\"type\":\"logic\",\"delay\":1.5,\"from\":{\"cell\":\"a\",\"port\":\"Q\"},\"to\":{\"cell\":\"b\",\"port\":\"D\"},\"net\":\"n\"}]}]}"; + +fn w_has(o: []const u8, n: u32, want: []const u8) -> bool { + if (want.len > (n as usize)) { return false; } + var i : usize = 0; + while (i + want.len <= (n as usize)) { + var k : usize = 0; + while (k < want.len and o[i + k] == want[k]) { k = k + 1; } + if (k == want.len) { return true; } + i = i + 1; + } + return false; +} + +fn open_page() -> u32 { + w_init(""); + w_view(600.0, 1.0, 0, 768.0, 600.0); + return 0; +} + +test the_page_opens_loading_with_the_demo_button_hidden { + open_page(); + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "

    Loading the paths...

    "); + assert w_has(o[0..], n, ""); + assert w_has(o[0..], n, ""); + assert !w_has(o[0..], n, "width="); + assert w_bdraw(o[0..]) == 0; +} + +test a_timing_json_is_sorted_worst_first_and_summarised { + open_page(); + w_file("t.json", 100.0, W_TEST_OWN, 0); + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "

    t.json: 2 paths, worst slack -0.005 ns at 50 MHz

    "); + assert w_has(o[0..], n, "

    Path 1 of 2: a.Q

    "); + assert w_has(o[0..], n, "

    total 20.005 ns = 20.005 logic + 0.000 routing; slack -0.005 ns

    "); + assert w_has(o[0..], n, "clk209.7850.00PASS"); + assert w_has(o[0..], n, " style=\"max-height: 461px;\""); + assert w_has(o[0..], n, " width=\"600\" height=\"34\" style=\"height: 34px;\""); + assert w_has(o[0..], n, ""); +} + +test the_keys_move_the_pick_and_ask_for_a_scroll { + open_page(); + w_file("t.json", 100.0, W_TEST_OWN, 0); + var e : [128]u8 = [0; 128]; + var n : u32 = w_effect(0, e[0..]); + assert e[0] == 120; + assert w_effect(1, e[0..]) == 0; + assert w_key("End") == 1; + assert PICK == 1; + n = w_effect(0, e[0..]); + assert e[0] == 121; + assert w_has(e[0..], n, ".sw-wrap\n25\n7"); + assert w_effect(1, e[0..]) == 0; + assert w_key("Home") == 1; + assert PICK == 0; + assert w_key("a") == 0; + var o : [65536]u8 = [0; 65536]; + const rn : u32 = w_render(o[0..]); + assert w_has(o[0..], rn, "Path 1 of 2: a.Q"); + assert w_key("ArrowDown") == 1; + const r2 : u32 = w_render(o[0..]); + assert w_has(o[0..], r2, "

    Path 2 of 2: b.D

    "); +} + +test a_press_on_the_canvas_picks_the_row_under_it { + open_page(); + w_file("t.json", 100.0, W_TEST_OWN, 0); + assert w_cpointer(0, 1, 5.0, 26.0, 1) == 1; + assert PICK == 1; + assert w_cpointer(0, 1, 5.0, 4.0, 1) == 0; + assert w_cpointer(0, 1, 5.0, 400.0, 1) == 0; + assert w_cpointer(1, 1, 5.0, 18.0, 1) == 0; + assert PICK == 1; +} + +test full_and_fit_change_the_range_and_the_buttons { + open_page(); + w_file("t.json", 100.0, W_TEST_OWN, 0); + assert X_MAX > 20405.0 and X_MAX < 20405.2; + w_click(1); + assert X_MAX > 21205.2 and X_MAX < 21205.4; + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "type=\"button\" aria-pressed=\"false\">Full period"); + assert w_has(o[0..], n, "type=\"button\" aria-pressed=\"true\">Fit paths"); +} + +test a_nextpnr_report_is_read_with_its_clock_constraint { + open_page(); + w_file("r.json", 100.0, W_TEST_REPORT, 0); + var o : [65536]u8 = [0; 65536]; + const n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "r.json: 1 paths, worst slack +18.500 ns at 50 MHz"); + assert w_has(o[0..], n, "Path 1 of 1: b.D (clk -> clk)"); + assert w_has(o[0..], n, "a.Qb.Dn"); +} + +test a_file_that_is_not_a_timing_file_says_why { + open_page(); + var o : [65536]u8 = [0; 65536]; + w_file("x", 5.0, "oops", 0); + var n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, ">x is not JSON.

    "); + w_file("y", 2.0, "{}", 0); + n = w_render(o[0..]); + assert w_has(o[0..], n, ">y is neither a nextpnr report (critical_paths, fmax) nor a timing.json.

    "); + w_file("big", 9000000.0, "", 0); + n = w_render(o[0..]); + assert w_has(o[0..], n, ">big is larger than 8 MB.

    "); + w_file("z", 5.0, "{\"critical_paths\":[null]}", 0); + n = w_render(o[0..]); + assert w_has(o[0..], n, ">Reading z...

    "); + w_file("q", 5.0, "{}", 1); + n = w_render(o[0..]); + assert w_has(o[0..], n, ">q is not JSON.

    "); +} + +test the_waterfall_asks_the_host_for_log10_and_pow_before_it_draws { + open_page(); + w_file("t.json", 100.0, W_TEST_OWN, 0); + var o : [262144]u8 = [0; 262144]; + assert w_bdraw(o[0..]) == 0; + var q : [64]f64 = [0.0; 64]; + const c : u32 = w_libm_need(q[0..]); + assert c == 2; + assert q[0] == 4.0; + assert q[3] == 5.0; + assert q[4] == 10.0; +} + +test the_drop_label_follows_the_drag { + open_page(); + assert w_drag(0) == 1; + assert w_drag(0) == 0; + var o : [65536]u8 = [0; 65536]; + var n : u32 = w_render(o[0..]); + assert w_has(o[0..], n, "