├── .envrc ├── .gitattributes ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── ARCHITECTURE_DRAFT.md ├── LICENSE ├── README.md ├── TODO.md ├── experimentation.ipynb ├── media ├── CPU Logo Favicon.png ├── CPU Logo.png ├── CompilationPipelineVert.svg ├── Header.png ├── State Four.png ├── State One.png ├── State Three.png ├── State Two.png ├── State Zero.png ├── U States.png ├── presentation.typ ├── solved-goto-failure-dark.png └── solved-goto-success-dark.png ├── rust-toolchain.toml ├── shell.nix └── src ├── Cargo.lock ├── Cargo.toml ├── cli ├── Cargo.toml └── src │ ├── main.rs │ └── robot.rs ├── clippy.toml ├── compiler ├── Cargo.toml ├── qat.pest ├── src │ ├── builtin_macros.rs │ ├── lib.rs │ ├── lua.rs │ ├── macro_expansion.rs │ ├── parsing.rs │ └── strip_expanded.rs └── tests │ ├── average │ ├── average.q │ └── average_transform.qat │ ├── fib │ ├── fib.q │ ├── fib.qat │ └── fib_transform.qat │ ├── multiply │ ├── multiply.c │ ├── multiply.q │ └── multiply_transform.qat │ └── simple │ ├── simple.q │ └── simple.qat ├── cycle_combination_solver ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs └── src │ ├── lib.rs │ ├── phase1 │ ├── .gitignore │ ├── best_lcm_testing.py │ ├── common_types.py │ ├── efficient_combination_structures.py │ ├── equivalent_combination_structures.py │ ├── mod.rs │ ├── optimal_combination_structures.py │ ├── optimal_combination_structures_landau.py │ ├── puzzle_orbit_definitions.py │ └── test_phase1.py │ ├── phase2-GAP-experimental │ ├── phase2.g │ ├── phase3.g │ ├── reader.g │ ├── testutil.g │ └── util.g │ └── phase2 │ ├── canonical_fsm.rs │ ├── mod.rs │ ├── orbit_puzzle.rs │ ├── orbit_puzzle │ ├── cube3 │ │ ├── avx2.rs │ │ ├── mod.rs │ │ └── simd8and16.rs │ └── slice_orbit_puzzle.rs │ ├── permutator.rs │ ├── pruning.rs │ ├── pruning_simulation.py │ ├── puzzle.rs │ ├── puzzle │ ├── cube3 │ │ ├── avx2.rs │ │ ├── mod.rs │ │ └── simd8and16.rs │ └── slice_puzzle.rs │ ├── puzzle_state_history.rs │ └── solver.rs ├── interpreter ├── Cargo.toml └── src │ └── lib.rs ├── movecount_coefficient ├── Cargo.toml └── src │ └── lib.rs ├── pog_ans ├── Cargo.toml └── src │ └── lib.rs ├── puzzle_geometry ├── Cargo.toml └── src │ ├── edge_cloud.rs │ ├── knife.rs │ ├── ksolve.rs │ ├── lib.rs │ └── shapes.rs └── qter_core ├── Cargo.toml ├── prelude.qat ├── puzzles ├── 210-24.bin ├── 30-30-30.bin └── 3x3.txt └── src ├── architectures.rs ├── lib.rs ├── math ├── discrete_math.rs ├── mod.rs ├── numbers.rs ├── schreier_sims.rs └── union_find.rs ├── puzzle.pest ├── puzzle_parser.rs ├── runtime.rs ├── shared_facelet_detection.rs ├── span.rs └── table_encoding.rs /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # the shebang is ignored, but nice for editors 3 | 4 | if type -P lorri &>/dev/null; then 5 | eval "$(lorri direnv)" 6 | else 7 | if type -P nix_direnv_manual_reload &>/dev/null; then 8 | nix_direnv_manual_reload 9 | fi 10 | 11 | use nix 12 | fi 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/**/*.q linguist-vendored 2 | src/**/*.qat linguist-language=janet 3 | src/**/*.qat linguist-vendored -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */target 2 | .DS_Store 3 | .direnv 4 | src/cli/*.qat 5 | src/cli/test.* 6 | .ipynb_checkpoints 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/three_phase_solver/phase3/vcube"] 2 | path = src/cycle_combination_solver/src/phase2-GAP-experimental/vcube 3 | url = https://github.com/ArhanChaudhary/vcube 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["qter"], 3 | "cSpell.ignorePaths": ["src"], 4 | "markdownlint.config": { 5 | "MD001": false, 6 | "MD009": false, 7 | "MD024": { "siblings_only": true }, 8 | "MD025": false, 9 | "MD033": false, 10 | "MD041": false 11 | }, 12 | "rust-analyzer.server.extraEnv": { 13 | "RUSTFLAGS": "-Ctarget-cpu=native" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ARCHITECTURE_DRAFT.md: -------------------------------------------------------------------------------- 1 | Ok so, here's an overview of the qter repository and compilation pipeline. Forgive me if this is TMI, feel free to dump this into chatgippity and ask for a TLDR. 2 | 3 | # First, the packages: 4 | 5 | `qter_core` defines a variety of common types, implementations of mathematical functions, and code to operate on twisty puzzles. 6 | `cli` implements the command line interface for qter 7 | `compiler` implements the compilation process from `QAT` to the intermediate representation defined in `qter_core/runtime.rs` 8 | `interpreter` implements an interpreter of the intermediate representation 9 | 10 | The other packages are more math heavy and you won't have to worry about them unless you want to: 11 | 12 | `puzzle_geometry` allows a twisty puzzle to be defined as a polyhedron with cut planes. It's mostly computational geometry, and converts a geometrical representation of a puzzle into a permutation group representation of the puzzle 13 | `cycle_combination_solver` implements the search process for algs to encode registers 14 | `movecount_coefficient` implements a metric for how "easy" it is for a human to perform an algorithm (sequence of moves) on a rubik's cube. For example, turns on the back face are harder to do than turns on the top face, though it gets more complicated than that. 15 | 16 | # Second, some important technical details not mentioned in the readme: 17 | 18 | Note that when I wrote the compiler, we hadn't had the idea of memory tapes yet and instead we were planning on implementing a system where the `.registers` declaration would be allowed to be placed anywhere and execution of the declaration would push puzzles to a global stack. We decided that memory tapes were better because this system only makes qter equivalent to a pushdown automaton whereas memory tapes make it equivalent to a turing machine. The point is that many of the types as well as the language specification assume that `.registers` can appear anywhere. At some point we will have to get rid of that but if you see strange type definitions, this may be why. Thankfully the code only partially supported this so there will not be too much to remove. 19 | 20 | Another concept not mentioned in the readme that is important to understanding the compiler is architecture switching. The concept is basically that if a register is zero, then all of its pieces are solved. What you can do is take all of those pieces and do a different algorithm to affect them such that the register has a different order. Say your program has been operating in the (30, 30, 30) architecture but at a particular point in the computation needs to represent a number greater than 30. What it can do is zero out two of the 30 order registers, and reuse those pieces for a single 180 order register. Note that the process will actually involve tranferring the information in the 30 order register to a new 30 order register since the structure of the new 30 order register needs to be different for the 180 order register to fit but that's more on the technical side. This is unimplemented for now but it prevents the compiler from making as many assumptions as you might think it could. 21 | 22 | We also thought of new instructions since writing the readme that will help humans read the Q file. `solve-puzzle` asks the human to bring the puzzle to the solved state, and `repeat-until` asks the human to repeat a sequence of moves until particular pieces are solved. 23 | 24 | QAT has syntax for defining procedural macros using inline Lua code that is executed by the compiler. Basically if you see lua stuff, that's what's going on. If you want to see the syntax for this, you can look in `src/qter_core/prelude.qat` which defines the standard library of macros. 25 | 26 | For strings, we use a thing called `ArcIntern` which is a reference counting pointer that automatically deduplicates equal instances. This way, equality, hashing, and cloning can be done just on the pointer so are very efficient. The code will even regularly use the entire file contents as a proxy for the file itself. 27 | 28 | # Third, the compilation pipeline: 29 | 30 | The pipeline starts in the CLI when the user types in something line `qter interpret file.qat`. If you like, you can `cd` into `src/cli`, copy/paste the example in the qter readme into a .qat file, and do `cargo run -- interpret file.qat`. You should be able to run the program. Most of the CLI is unimplemented right now so it only supports interpretation of qat files. The CLI will call into the compiler package to transform your program into the intermediate representation. 31 | 32 | The first step is to parse your code. This is done using a library called `pest`, which allows you to define a grammar for a language and it transforms that into parsing code. The QAT language is defined at `src/compiler/qat.pest`. `pest` will return an untyped syntax tree, so the next step is to transform that syntax tree into meaningful, typed data. `compiler/src/parsing.rs` performs this task. It also takes all of the inline lua and merges it into a lua program. 33 | 34 | The next step is to perform macro expansion, which is done in `compiler/src/macro_expansion.rs`. This is for the most part unimplemented, except for the "builtin macros" which have a one-to-one mapping with Q instructions. The builtin macros are defined in `compiler/src/builtin_macros.rs`, which contains functions that each manually parse the macro arguments and turn them into instructions. 35 | 36 | Now that we have flattened the program into a string of labels and instructions, the final step is to clean everything up and to perform optimizations. This is done in `compiler/src/strip_expanded.rs`. You may notice that the intermediate representation does not have labels, and instead jump instructions directly jump to instruction indices. This is the step that substitutes label names for instruction indices. It will also perform validation and optimizations ahead of that. Right now, the only thing it does is coalesce consecutive add instructions. On a Rubik's cube, any position can be solved in <=20 moves, therefore a long string of additions may be coalesced into a single sequence of <=20 moves. Right now it implements coalescing but not optimizing of move count, just appending the sequences together. It will eventually implement prevention of jumping to a location that assumes a different architecture, analysis of whether conditional jumps are guaranteed and removal of dead code, and searching for particular code patterns that implement the `solve-puzzle` or `repeat-until` instructions. 37 | 38 | That concludes the compilation pipeline as is implemented right now. To complete the compilation pipeline, the last step would be assembling the instructions into a .Q file. This should be fairly easy, a simple series of `writeln!` calls. Parsing the .Q file will also be necessary for interpreting it but this is also unimplemented. 39 | 40 | # Fourth, the interpreter: 41 | 42 | The interpreter is comparatively simple, it just runs through the instructions as outputted by the compiler. The `step` function returns a trace of what it is doing to allow for the future implementation of a debugger in the CLI. Right now, the CLI is capable of dumping this output to allow it to be given to a cubing robot. There's a fairly mathy optimization I would like to make to the interpreter that I can explain if you would like. 43 | 44 | I think that's a pretty good technical overview of the entire system. 45 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - ⭐crashlog 4 | 5 | ## SCC 6 | 7 | - Phase 2 8 | - ⭐pruning table 9 | - cartesian product tables 10 | - ⭐fix storage backend initialization meta 11 | - tANS table compression 12 | - ⭐cycle type pruning table 13 | - ⭐with fewer state spaces, go back to an exact pruning table 14 | - ⭐each thread fills in 1024 entires at a time 15 | - ⭐exact: dfs at low levels instead of scanning 16 | - dynamic simd detection rather than -Ctarget-cpu=native 17 | - ghostcell instead of unsafe replace compose 18 | - ⭐stabilizer 19 | - ⭐avoid symmetric moves from the start 20 | - fix corner in stabilizer for 4x4 21 | - ⭐solved state for 4x4 22 | - ⭐standard symmetry 23 | - ⭐antisymmetry 24 | - ⭐fix sequence symmetry 25 | - ⭐multithreading 26 | - microthreading 27 | - ⭐make fsm lookup unsafe when pg is done 28 | - ⭐Phase 1 29 | - Look into fixing a corner for even cubes/other puzzles 30 | - Schreier Sims & generating algs using it 31 | 32 | ## PuzzleGeometry 33 | 34 | - Get a permutation group out of a puzzle definition 35 | - Canonical ordering of stickers 36 | - ⭐Define the moves as permutations and orientations of pieces 37 | - ⭐Calculate orientations and parities of the puzzle 38 | - Calculate the symmetries of the puzzle 39 | - ⭐Parse our modified puzzlegeometry definition string 40 | - Release as a crate on crates.io 41 | 42 | ## QAT 43 | 44 | - ⭐Precompute tables for builtin architectures 45 | - ⭐Refactor register references so that they assume the register declaration is global 46 | - ⭐QAT Macros 47 | - ⭐Actual expansion 48 | - ⭐`after` syntax 49 | - ⭐Lua stuff 50 | - Architecture switching 51 | - ⭐Memory tapes 52 | - ⭐Implement in QAT 53 | - Dynamically shuffle sub-cycles with syntax X ← A\*B\*C\*D, Y ← E\*F\*G\*H 54 | - ⭐Function macro 55 | - ⭐Directory of testing programs instead of hardcoding into Rust 56 | - ⭐Inline testing in the QAT format 57 | - `solve-puzzle` and instruction to copy solving moves to other puzzle 58 | - Analyzing branches and removing dead code 59 | - Architecture that avoids sharing a piece by always having two additions simultaneously which avoids parity 60 | 61 | ## Interpreter/CLI 62 | 63 | - Implement tapes 64 | - Debugging tool 65 | - Implementing the fancy CRT/loop-repetition-calculating thingy 66 | 67 | ## Q 68 | 69 | - ⭐Compile to Q 70 | - ⭐Parse Q 71 | - ⭐"[repeat|print|halt] until _ solved" syntax 72 | - ⭐optimize out immediate gotos after a label 73 | - ⭐Asher's repeated move post process optimization: R U R repeated = R then U R2 repeated then R' 74 | - ⭐force conditional blocks that end with "halt" to codegen at the end of the instruction memory, optimizing a goto 75 | 76 | ## End user 77 | 78 | - Web app of qter with a visualization 79 | - ⭐Youtube videos 80 | 81 | ## Robot 82 | 83 | - ⭐Add robot to the README 84 | -------------------------------------------------------------------------------- /experimentation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 124, 6 | "id": "5d3ede31-3a55-4404-9ce0-5b9ad9af747a", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "name": "stdout", 11 | "output_type": "stream", 12 | "text": [ 13 | "1: 18 (truth)\n", 14 | "1: 18 (0.00%)\n", 15 | "\n", 16 | "2: 243 (truth)\n", 17 | "2: 243 (0.00%)\n", 18 | "\n", 19 | "3: 3240 (truth)\n", 20 | "3: 3240 (0.00%)\n", 21 | "\n", 22 | "4: 43239 (truth)\n", 23 | "4: 43254 (0.03% ∨)\n", 24 | "\n", 25 | "5: 574908 (truth)\n", 26 | "5: 577368 (0.43% ∨)\n", 27 | "\n", 28 | "6: 7618438 (truth)\n", 29 | "6: 7706988 (1.16% ∨)\n", 30 | "\n", 31 | "7: 100803036 (truth)\n", 32 | "7: 102876480 (2.06% ∨)\n", 33 | "\n", 34 | "8: 1332343288 (truth)\n", 35 | "8: 1373243544 (3.07% ∨)\n", 36 | "\n", 37 | "9: 17596479795 (truth)\n", 38 | "9: 18330699163 (4.17% ∨)\n", 39 | "\n", 40 | "10: 232248063316 (truth)\n", 41 | "10: 244686772944 (5.36% ∨)\n", 42 | "\n", 43 | "11: 3063288809012 (truth)\n", 44 | "11: 3266193716687 (6.62% ∨)\n", 45 | "\n", 46 | "12: 40374425656248 (truth)\n", 47 | "12: 43598660931314 (7.99% ∨)\n", 48 | "\n", 49 | "13: 531653418284628 (truth)\n", 50 | "13: 581970859874599 (9.46% ∨)\n", 51 | "\n", 52 | "14: 6989320578825358 (truth)\n", 53 | "14: 7767614087210713 (11.14% ∨)\n", 54 | "\n", 55 | "15: 91365146187124320 (truth)\n", 56 | "15: 103542280201681184 (13.33% ∨)\n", 57 | "\n", 58 | "16: 1100000000000000000 (truth)\n", 59 | "16: 1356899887946458880 (23.35% ∨)\n", 60 | "\n", 61 | "17: 12000000000000000000 (truth)\n", 62 | "17: 14311788362567002112 (19.26% ∨)\n", 63 | "\n", 64 | "18: 29000000000000000000 (truth)\n", 65 | "18: 27164778495788613632 (6.33% ∧)\n", 66 | "\n", 67 | "19: 1500000000000000000 (truth)\n", 68 | "19: 306578611711995904 (79.56% ∧)\n", 69 | "\n", 70 | "20: 490000000 (truth)\n", 71 | "20: 18741879355712 (3824773.34% ∨)\n", 72 | "\n", 73 | "21: 180078365151\n", 74 | "22: 12622059\n", 75 | "23: 108301\n" 76 | ] 77 | } 78 | ], 79 | "source": [ 80 | "import math\n", 81 | "\n", 82 | "dists_3x3 = [\n", 83 | " 1,\n", 84 | " 18,\n", 85 | " 243,\n", 86 | " 3240,\n", 87 | " 43239,\n", 88 | " 574908,\n", 89 | " 7618438,\n", 90 | " 100803036,\n", 91 | " 1332343288,\n", 92 | " 17596479795,\n", 93 | " 232248063316,\n", 94 | " 3063288809012,\n", 95 | " 40374425656248,\n", 96 | " 531653418284628,\n", 97 | " 6989320578825358,\n", 98 | " 91365146187124320,\n", 99 | " 1100000000000000000,\n", 100 | " 12000000000000000000,\n", 101 | " 29000000000000000000,\n", 102 | " 1500000000000000000,\n", 103 | " 490000000,\n", 104 | "]\n", 105 | "\n", 106 | " \n", 107 | "def entries_to_bits(size, entries):\n", 108 | " formula = size * (1 - e^(-x / size))\n", 109 | " formula_taylor = formula.taylor(x, 0, 10)\n", 110 | " return (formula_taylor if entries / size < 0.0001 else formula).subs(x=entries)\n", 111 | "\n", 112 | "def bits_to_entries(size, bits):\n", 113 | " formula_inv = -size * ln(1 - x/size)\n", 114 | " formula_inv_taylor = formula_inv.taylor(x, 0, 10)\n", 115 | " return (formula_inv_taylor if bits / size < 0.0001 else formula_inv).subs(x=bits)\n", 116 | "\n", 117 | "def estimate_dists(size, move_vector, move_matrix, expected_dists = []):\n", 118 | " prev_counts = 1\n", 119 | " prev_count_at_depth = 1\n", 120 | " too_far = 0\n", 121 | " i = 0\n", 122 | " disallowed_branching = 0\n", 123 | " \n", 124 | " while (size - too_far) >= (prev_count_at_depth + 0.5):\n", 125 | " branching_factor = sum(move_vector) / prev_counts\n", 126 | "\n", 127 | " effective_size = size - too_far\n", 128 | " amt_to_add = prev_count_at_depth * (branching_factor - disallowed_branching)\n", 129 | " entries = bits_to_entries(effective_size, prev_count_at_depth) + amt_to_add\n", 130 | " \n", 131 | " values_without = entries_to_bits(effective_size, amt_to_add)\n", 132 | " disallowed_branching = amt_to_add / values_without - 1\n", 133 | " #print(disallowed_branching)\n", 134 | " count_at_depth = n(entries_to_bits(effective_size, entries) - prev_count_at_depth)\n", 135 | " too_far += prev_count_at_depth\n", 136 | " \n", 137 | " i += 1\n", 138 | "\n", 139 | " if i < len(expected_dists):\n", 140 | " error = (count_at_depth - expected_dists[i]) / dists[i] * 100\n", 141 | " print(f\"{i}: {expected_dists[i]} (truth)\")\n", 142 | " print(f\"{i}: {round(count_at_depth)} ({float(abs(error)):.2f}%{\"\" if error == 0 else (\" ∧\" if error < 0 else \" ∨\")})\\n\")\n", 143 | " else:\n", 144 | " print(f\"{i}: {round(count_at_depth)}\")\n", 145 | "\n", 146 | " prev_counts = sum(move_vector)\n", 147 | " move_vector = move_matrix * move_vector\n", 148 | " prev_count_at_depth = count_at_depth\n", 149 | "\n", 150 | "#estimate_dists(274337280000, vector([3, 3]), matrix([[0, 3], [3, 0]]), [])\n", 151 | "estimate_dists(43252003274489860000, vector([3, 3, 3, 3, 3, 3]), matrix([[0, 3, 3, 3, 3, 3], [3, 0, 3, 3, 3, 3], [3, 3, 0, 3, 3, 3], [0, 3, 3, 0, 3, 3], [3, 0, 3, 3, 0, 3], [3, 3, 0, 3, 3, 0]]), dists_3x3)" 152 | ] 153 | } 154 | ], 155 | "metadata": { 156 | "kernelspec": { 157 | "display_name": "SageMath 10.5", 158 | "language": "sage", 159 | "name": "sagemath" 160 | }, 161 | "language_info": { 162 | "codemirror_mode": { 163 | "name": "ipython", 164 | "version": 3 165 | }, 166 | "file_extension": ".py", 167 | "mimetype": "text/x-python", 168 | "name": "python", 169 | "nbconvert_exporter": "python", 170 | "pygments_lexer": "ipython3", 171 | "version": "3.12.8" 172 | } 173 | }, 174 | "nbformat": 4, 175 | "nbformat_minor": 5 176 | } 177 | -------------------------------------------------------------------------------- /media/CPU Logo Favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/CPU Logo Favicon.png -------------------------------------------------------------------------------- /media/CPU Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/CPU Logo.png -------------------------------------------------------------------------------- /media/Header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/Header.png -------------------------------------------------------------------------------- /media/State Four.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/State Four.png -------------------------------------------------------------------------------- /media/State One.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/State One.png -------------------------------------------------------------------------------- /media/State Three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/State Three.png -------------------------------------------------------------------------------- /media/State Two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/State Two.png -------------------------------------------------------------------------------- /media/State Zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/State Zero.png -------------------------------------------------------------------------------- /media/U States.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/U States.png -------------------------------------------------------------------------------- /media/solved-goto-failure-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/solved-goto-failure-dark.png -------------------------------------------------------------------------------- /media/solved-goto-success-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/media/solved-goto-success-dark.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-03-02" 3 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | 5 | let 6 | rust_overlay = import ( 7 | builtins.fetchTarball { 8 | url = "https://github.com/oxalica/rust-overlay/archive/aefb7017d710f150970299685e8d8b549d653649.tar.gz"; 9 | sha256 = "sha256:0bwxwmbg3jnyiadn6bjk6sx2as0l9slzvp0xkx16jjr8bl8z0sz7"; 10 | } 11 | ); 12 | pkgs = import { overlays = [ rust_overlay ]; }; 13 | rust = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { 14 | extensions = [ 15 | "rust-src" 16 | "rust-analyzer" 17 | ]; 18 | }; 19 | in 20 | pkgs.mkShell { 21 | buildInputs = 22 | [ 23 | rust 24 | ] 25 | ++ (with pkgs; [ 26 | pkg-config 27 | sccache 28 | 29 | (gap.overrideAttrs (o: { 30 | version = "4.13.1"; 31 | patches = [ ]; 32 | src = fetchurl { 33 | url = "https://github.com/gap-system/gap/releases/download/v4.13.1/gap-4.13.1.tar.gz"; 34 | sha256 = "sha256-l5Tb26b7mY4KLQqoziH8iEitPT+cyZk7C44gvn4dvro="; 35 | }; 36 | })) 37 | 38 | (python3.withPackages ( 39 | p: with p; [ 40 | sympy 41 | ] 42 | )) 43 | python312Packages.python-lsp-server 44 | ]); 45 | 46 | RUST_BACKTRACE = 1; 47 | RUSTC_WRAPPER = "sccache"; 48 | SCCACHE_SERVER_PORT = "54226"; 49 | } 50 | -------------------------------------------------------------------------------- /src/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "qter_core", 5 | "interpreter", 6 | "compiler", 7 | "cli", 8 | "cycle_combination_solver", 9 | "puzzle_geometry", 10 | "movecount_coefficient", 11 | "pog_ans", 12 | ] 13 | 14 | [workspace.lints.clippy] 15 | type_complexity = "allow" 16 | disallowed_types = "deny" 17 | 18 | # [profile.release] 19 | # debug = true 20 | -------------------------------------------------------------------------------- /src/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | clap = { version = "4.5", features = [ "derive" ] } 8 | color-eyre = "0.6" 9 | itertools = "0.13" 10 | owo-colors = "4.2.0" 11 | qter_core = { path = "../qter_core" } 12 | compiler = { path = "../compiler" } 13 | interpreter = { path = "../interpreter" } 14 | puzzle_geometry = { path = "../puzzle_geometry" } 15 | internment = { version = "0.8", features = [ "arc" ] } 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [[bin]] 21 | name = "qter" 22 | path = "src/main.rs" 23 | -------------------------------------------------------------------------------- /src/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic)] 2 | #![allow(clippy::too_many_lines)] 3 | 4 | use std::{fs, io, path::PathBuf}; 5 | 6 | use clap::{ArgAction, Parser}; 7 | use color_eyre::{ 8 | eyre::{OptionExt, eyre}, 9 | owo_colors::OwoColorize, 10 | }; 11 | use compiler::compile; 12 | use internment::ArcIntern; 13 | use interpreter::{ 14 | ActionPerformed, ExecutionState, Interpreter, PausedState, PuzzleState, SimulatedPuzzle, 15 | }; 16 | use itertools::Itertools; 17 | use qter_core::{ 18 | I, Int, 19 | architectures::Algorithm, 20 | table_encoding::{decode_table, encode_table}, 21 | }; 22 | use robot::Cube3Robot; 23 | 24 | mod robot; 25 | 26 | /// Compiles and interprets qter programs 27 | #[derive(Parser)] 28 | #[command(version, about)] 29 | enum Commands { 30 | /// Compile a QAT file to Q 31 | Compile { 32 | /// Which file to compile; must be a .q file 33 | file: PathBuf, 34 | }, 35 | /// Interpret a QAT or a Q file 36 | Interpret { 37 | /// Which file to interpret; must be a .qat or .q file 38 | file: PathBuf, 39 | /// The level of execution trace to send to stderr. Can be set zero to three times. 40 | #[arg(short, action = ArgAction::Count)] 41 | trace_level: u8, 42 | #[arg(long)] 43 | robot: bool, 44 | }, 45 | /// Step through a QAT or a Q program 46 | Debug { 47 | /// Which file to interpret; must be a .qat or .q file 48 | file: PathBuf, 49 | }, 50 | /// Evaluate unit tests in a QAT program 51 | Test { 52 | /// Which file to test; must be a .qat file 53 | file: PathBuf, 54 | }, 55 | #[cfg(debug_assertions)] 56 | /// Compress an algorithm table into the special format (This subcommand will not be visible in release mode) 57 | Compress { 58 | /// The input alg table 59 | input: PathBuf, 60 | /// The output compressed data 61 | output: PathBuf, 62 | }, 63 | #[cfg(debug_assertions)] 64 | /// Print the contents of a compressed algorithm table to stdout (This subcommand will not be visible in release mode) 65 | Dump { 66 | /// The input alg table 67 | input: PathBuf, 68 | }, 69 | } 70 | 71 | fn main() -> color_eyre::Result<()> { 72 | let args = Commands::parse(); 73 | 74 | match args { 75 | Commands::Compile { file: _ } => todo!(), 76 | Commands::Interpret { 77 | file, 78 | trace_level, 79 | robot, 80 | } => { 81 | let program = match file.extension().and_then(|v| v.to_str()) { 82 | Some("q") => todo!(), 83 | Some("qat") => { 84 | let qat = fs::read_to_string(file)?; 85 | 86 | compile(&qat, |name| { 87 | let path = PathBuf::from(name); 88 | 89 | if path.ancestors().count() > 1 { 90 | // Easier not to implement relative paths and stuff 91 | return Err("Imported files must be in the same path".to_owned()); 92 | } 93 | 94 | match fs::read_to_string(path) { 95 | Ok(s) => Ok(ArcIntern::from(s)), 96 | Err(e) => Err(e.to_string()), 97 | } 98 | })? 99 | } 100 | _ => { 101 | return Err(eyre!( 102 | "The file {file:?} must have an extension of `.qat` or `.q`." 103 | )); 104 | } 105 | }; 106 | 107 | if robot { 108 | let interpreter = Interpreter::::new(program); 109 | interpret(interpreter, trace_level)?; 110 | } else { 111 | let interpreter = Interpreter::::new(program); 112 | interpret(interpreter, trace_level)?; 113 | } 114 | } 115 | Commands::Debug { file: _ } => todo!(), 116 | Commands::Test { file: _ } => todo!(), 117 | #[cfg(debug_assertions)] 118 | Commands::Compress { input, output } => { 119 | let data = fs::read_to_string(input)?; 120 | 121 | let to_encode = data 122 | .split('\n') 123 | .map(str::trim) 124 | .filter(|v| !v.is_empty()) 125 | .map(|alg| { 126 | alg.split_whitespace() 127 | .filter(|v| !v.is_empty()) 128 | .map(ArcIntern::from) 129 | .collect_vec() 130 | }) 131 | .collect_vec(); 132 | 133 | // for alg in &to_encode { 134 | // println!("{}", alg.iter().join(" ")); 135 | // } 136 | 137 | let (data, _) = 138 | encode_table(&to_encode).ok_or_eyre("Too many unique generators, contact Henry")?; 139 | 140 | fs::write(output, data)?; 141 | } 142 | #[cfg(debug_assertions)] 143 | Commands::Dump { input } => { 144 | let data = fs::read(input)?; 145 | 146 | let decoded = 147 | decode_table(&mut data.iter().copied()).ok_or_eyre("Could not decode the table")?; 148 | 149 | for moves in decoded { 150 | println!("{}", moves.iter().join(" ")); 151 | } 152 | } 153 | } 154 | 155 | Ok(()) 156 | } 157 | 158 | fn interpret( 159 | mut interpreter: Interpreter

, 160 | trace_level: u8, 161 | ) -> color_eyre::Result<()> { 162 | if trace_level > 0 { 163 | return interpret_traced(interpreter, trace_level); 164 | } 165 | loop { 166 | let paused_state = interpreter.step_until_halt(); 167 | 168 | let is_input_state = matches!( 169 | paused_state, 170 | PausedState::Input { 171 | max_input: _, 172 | puzzle_idx: _, 173 | register: _ 174 | } 175 | ); 176 | 177 | while let Some(message) = interpreter.messages().pop_front() { 178 | println!("{message}"); 179 | } 180 | 181 | if is_input_state { 182 | give_number_input(&mut interpreter)?; 183 | } else { 184 | break Ok(()); 185 | } 186 | } 187 | } 188 | 189 | fn give_number_input( 190 | interpreter: &mut Interpreter

, 191 | ) -> color_eyre::Result<(usize, Option)> { 192 | loop { 193 | let mut number = String::new(); 194 | io::stdin().read_line(&mut number)?; 195 | match number.parse::>() { 196 | Ok(value) => match interpreter.give_input(value) { 197 | Ok((puzzle_idx, exponentiated_alg)) => { 198 | break Ok((puzzle_idx, exponentiated_alg)); 199 | } 200 | Err(e) => println!("{e}"), 201 | }, 202 | Err(_) => println!("Please input an integer"), 203 | } 204 | } 205 | } 206 | 207 | fn interpret_traced( 208 | mut interpreter: Interpreter

, 209 | trace_level: u8, 210 | ) -> color_eyre::Result<()> { 211 | loop { 212 | let program_counter = interpreter.program_counter() + 1; 213 | 214 | let action = interpreter.step(); 215 | 216 | if trace_level >= 3 { 217 | eprint!("{program_counter} | "); 218 | } 219 | 220 | let mut should_give_input = false; 221 | let mut halted = false; 222 | 223 | match action { 224 | ActionPerformed::None => { 225 | if trace_level >= 2 { 226 | eprintln!("Printing"); 227 | } 228 | } 229 | ActionPerformed::Paused => { 230 | let is_input = matches!( 231 | interpreter.execution_state(), 232 | ExecutionState::Paused(PausedState::Input { 233 | max_input: _, 234 | puzzle_idx: _, 235 | register: _ 236 | }) 237 | ); 238 | 239 | if is_input { 240 | if trace_level >= 2 { 241 | eprintln!("Accepting input"); 242 | } 243 | 244 | should_give_input = true; 245 | } else { 246 | if trace_level >= 2 { 247 | eprintln!("Halting"); 248 | } 249 | 250 | halted = true; 251 | } 252 | } 253 | ActionPerformed::Goto { instruction_idx: _ } => { 254 | if trace_level >= 3 { 255 | eprintln!("Jumping"); 256 | } 257 | } 258 | ActionPerformed::FailedSolvedGoto { 259 | puzzle_idx, 260 | facelets: _, 261 | } => { 262 | if trace_level >= 2 { 263 | eprintln!("Inspect puzzle {puzzle_idx} - {}", "NOT TAKEN".red()); 264 | } 265 | } 266 | ActionPerformed::SucceededSolvedGoto { 267 | puzzle_idx, 268 | facelets: _, 269 | instruction_idx: _, 270 | } => { 271 | if trace_level >= 2 { 272 | eprintln!("Inspect puzzle {puzzle_idx} - {}", "TAKEN".green()); 273 | } 274 | } 275 | ActionPerformed::AddedToTheoretical { 276 | puzzle_idx: register_idx, 277 | amt, 278 | } => { 279 | eprintln!("Theoretical {register_idx} += {amt}"); 280 | } 281 | ActionPerformed::ExecutedAlgorithm { 282 | puzzle_idx, 283 | algorithm, 284 | } => { 285 | eprint!("Puzzle {puzzle_idx}:"); 286 | 287 | for move_ in algorithm.move_seq_iter() { 288 | eprint!(" {move_}"); 289 | } 290 | 291 | eprintln!(); 292 | } 293 | ActionPerformed::Panicked => { 294 | eprintln!("{}", "Panicked!".red()); 295 | halted = true; 296 | } 297 | } 298 | 299 | while let Some(interpreter_message) = interpreter.messages().pop_front() { 300 | println!("{interpreter_message}"); 301 | } 302 | 303 | if halted { 304 | break Ok(()); 305 | } 306 | 307 | if should_give_input { 308 | let (puzzle_idx, exponentiated_alg) = give_number_input(&mut interpreter)?; 309 | 310 | let Some(exponentiated_puzzle_alg) = exponentiated_alg else { 311 | continue; 312 | }; 313 | 314 | eprint!("Puzzle {puzzle_idx}:"); 315 | 316 | for move_ in exponentiated_puzzle_alg.move_seq_iter() { 317 | eprint!(" {move_}"); 318 | } 319 | 320 | eprintln!(); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/clippy.toml: -------------------------------------------------------------------------------- 1 | ignore-interior-mutability = [ "qter_core::WithSpan" ] 2 | disallowed-types = [ 3 | { path = "pest::error::ErrorVariant", reason = "Using the `error` function in `qter_core` is more concise" } 4 | ] 5 | -------------------------------------------------------------------------------- /src/compiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "compiler" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | pest = "2.7" 8 | pest_derive = "2.7" 9 | qter_core = { path = "../qter_core" } 10 | internment = { version = "0.8", features = [ "arc" ] } 11 | itertools = "0.13" 12 | mlua = { version = "0.10.2", features = ["lua54", "vendored", "send"] } 13 | 14 | [lints] 15 | workspace = true 16 | -------------------------------------------------------------------------------- /src/compiler/qat.pest: -------------------------------------------------------------------------------- 1 | WHITESPACE = _{ " " | "\t" } 2 | COMMENT = _{ ("--[[" ~ (!"--]]" ~ ANY)* ~ "--]]") | (("--" | (SOI ~ "#!")) ~ (!"\n" ~ ANY)* ~ "\n") } 3 | 4 | program = { "\n"* ~ statement ~ ("\n"+ ~ statement)* ~ "\n"* ~ EOI } 5 | 6 | special_char = { "lua" | "{" | "}" | "." | ":" | "$" | "--" | "," | "<-" | "←" | "\n" | "(" | ")" | "!" | "\"" | WHITESPACE } 7 | 8 | ident = @{ (!(number ~ WHITESPACE) ~ (!special_char ~ ANY)+) | ("\"" ~ (!"\"" ~ ANY)* ~ "\"") } 9 | tag_ident = @{ "!"? ~ ident } 10 | 11 | number = { ASCII_DIGIT+ } 12 | 13 | algorithm = { ident+ } 14 | 15 | registers = { ".registers" ~ "{" ~ "\n"? ~ register_declaration ~ ("\n"+ ~ register_declaration)* ~ "\n"* ~ "}" } 16 | register_declaration = _{ unswitchable | switchable } 17 | 18 | unswitchable = { ident ~ ("," ~ ident)* ~ ("<-" | "←") ~ register_architecture } 19 | switchable = { "(" ~ "\n"* ~ unswitchable ~ ( "\n"+ ~ unswitchable )* ~ "\n"* ~ ")" } 20 | 21 | builtin_architecture = { (!"builtin" ~ ident) ~ "builtin" ~ (number | ("(" ~ "\n"* ~ number ~ ("," ~ "\n"* ~ number)* ~ ","? ~ "\n"* ~ ")")) } 22 | custom_architecture = { ident ~ (ident+ | ("(" ~ "\n"* ~ algorithm ~ ("," ~ "\n"* ~ algorithm)* ~ ","? ~ "\n"* ~ ")")) } 23 | theoretical_architecture = { "theoretical" ~ number } 24 | real_architecture = { builtin_architecture | custom_architecture } 25 | register_architecture = _{ theoretical_architecture | real_architecture } 26 | 27 | statement = _{ macro | instruction | lua_block | import } 28 | 29 | constant = { "$" ~ ident } 30 | value = { number | constant | ident | block } 31 | label = { tag_ident ~ ":" } 32 | code = { ident ~ value* } 33 | lua_call = { "lua" ~ ident ~ "(" ~ "\n"? ~ value? ~ ("," ~ "\n"? ~ value)* ~ ","? ~ "\n"? ~ ")" } 34 | instruction = { label | code | constant | lua_call | define | registers } 35 | 36 | type = { "block" | "reg" | "int" | "ident" } 37 | 38 | macro = { ".macro" ~ tag_ident ~ ("after" ~ tag_ident)? ~ "{" ~ ("\n"* ~ macro_branch)+ ~ "\n"* ~ "}" } 39 | macro_arg = ${ ident | (constant ~ ":" ~ type) } 40 | pattern = { macro_arg* } 41 | macro_branch = { "(" ~ pattern ~ ")" ~ "=>" ~ (block | instruction) } 42 | 43 | block = { "{" ~ "\n"* ~ statement? ~ ("\n"+ ~ statement)* ~ "\n"* ~ "}" } 44 | 45 | define = { ".define" ~ ident ~ (lua_call | value) } 46 | 47 | import_filename = @{ident ~ ".qat"} 48 | import = { ".import" ~ import_filename } 49 | 50 | lua_code = { (!"end-lua" ~ ANY)* } 51 | lua_block = _{ ".start-lua" ~ lua_code ~ "end-lua" } 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/compiler/src/builtin_macros.rs: -------------------------------------------------------------------------------- 1 | use internment::ArcIntern; 2 | use pest::error::Error; 3 | use qter_core::{Span, WithSpan, mk_error}; 4 | 5 | use crate::{ 6 | BlockID, Code, ExpansionInfo, Instruction, LabelReference, Macro, Primitive, RegisterReference, 7 | Value, parsing::Rule, 8 | }; 9 | 10 | use std::collections::HashMap; 11 | 12 | fn expect_reg( 13 | reg_value: &WithSpan, 14 | syntax: &ExpansionInfo, 15 | block_id: BlockID, 16 | ) -> Result>> { 17 | match &**reg_value { 18 | Value::Ident(reg_name) => match syntax.block_info.get_register( 19 | &RegisterReference::parse( 20 | block_id, 21 | WithSpan::new(ArcIntern::clone(reg_name), reg_value.span().to_owned()), 22 | ) 23 | .map_err(|e| { 24 | mk_error( 25 | format!("Could not parse the modulus as a string: {e}"), 26 | reg_value.span(), 27 | ) 28 | })?, 29 | ) { 30 | Some((reg, _)) => Ok(reg), 31 | None => Err(mk_error( 32 | format!("The register {reg_name} does not exist"), 33 | reg_value.span(), 34 | )), 35 | }, 36 | _ => Err(mk_error("Expected a register", reg_value.span())), 37 | } 38 | } 39 | 40 | fn expect_label( 41 | label_value: &WithSpan, 42 | block_id: BlockID, 43 | ) -> Result, Box>> { 44 | match &**label_value { 45 | Value::Ident(label_name) => Ok(WithSpan::new( 46 | LabelReference { 47 | name: ArcIntern::clone(label_name), 48 | block_id, 49 | }, 50 | label_value.span().to_owned(), 51 | )), 52 | _ => Err(mk_error("Expected a label", label_value.span())), 53 | } 54 | } 55 | 56 | fn print_like( 57 | syntax: &ExpansionInfo, 58 | mut args: WithSpan>>, 59 | block_id: BlockID, 60 | ) -> Result<(Option, WithSpan), Box>> { 61 | if args.len() > 2 { 62 | return Err(mk_error( 63 | format!("Expected one or two arguments, found {}", args.len()), 64 | args.span(), 65 | )); 66 | } 67 | 68 | let maybe_reg = if args.len() == 2 { 69 | Some(expect_reg(args.pop().as_ref().unwrap(), syntax, block_id)?) 70 | } else { 71 | None 72 | }; 73 | 74 | let message = args.pop().unwrap(); 75 | let span = message.span().to_owned(); 76 | let message = match message.into_inner() { 77 | Value::Ident(raw_message) => { 78 | if !raw_message.starts_with('"') || !raw_message.ends_with('"') { 79 | return Err(mk_error("The message must be quoted", span)); 80 | } 81 | 82 | let raw_message = raw_message.strip_prefix('"').unwrap_or(&raw_message); 83 | let raw_message = raw_message.strip_suffix('"').unwrap_or(raw_message); 84 | 85 | WithSpan::new(raw_message.to_owned(), span) 86 | } 87 | _ => { 88 | return Err(mk_error("Expected a message", span)); 89 | } 90 | }; 91 | 92 | Ok((maybe_reg, message)) 93 | } 94 | 95 | pub fn builtin_macros( 96 | prelude: &ArcIntern, 97 | ) -> HashMap<(ArcIntern, ArcIntern), WithSpan> { 98 | let mut macros = HashMap::new(); 99 | 100 | let dummy_span = Span::new(ArcIntern::from(" "), 0, 0); 101 | 102 | macros.insert( 103 | (prelude.clone(), ArcIntern::from("add")), 104 | WithSpan::new( 105 | Macro::Builtin(|syntax, mut args, block_id| { 106 | if args.len() != 2 { 107 | return Err(mk_error( 108 | format!("Expected two arguments, found {}", args.len()), 109 | args.span(), 110 | )); 111 | } 112 | 113 | let second_arg = args.pop().unwrap(); 114 | let amt = match *second_arg { 115 | Value::Int(int) => WithSpan::new(int, second_arg.span().to_owned()), 116 | _ => { 117 | return Err(mk_error("Expected a number", second_arg.span())); 118 | } 119 | }; 120 | 121 | let register = expect_reg(args.pop().as_ref().unwrap(), syntax, block_id)?; 122 | 123 | Ok(vec![Instruction::Code(Code::Primitive(Primitive::Add { 124 | amt, 125 | register, 126 | }))]) 127 | }), 128 | dummy_span.clone(), 129 | ), 130 | ); 131 | 132 | macros.insert( 133 | (prelude.to_owned(), ArcIntern::from("goto")), 134 | WithSpan::new( 135 | Macro::Builtin(|_syntax, mut args, block_id| { 136 | if args.len() != 1 { 137 | return Err(mk_error( 138 | format!("Expected one argument, found {}", args.len()), 139 | args.span(), 140 | )); 141 | } 142 | 143 | let label = expect_label(args.pop().as_ref().unwrap(), block_id)?; 144 | 145 | Ok(vec![Instruction::Code(Code::Primitive(Primitive::Goto { 146 | label, 147 | }))]) 148 | }), 149 | dummy_span.clone(), 150 | ), 151 | ); 152 | 153 | macros.insert( 154 | (prelude.to_owned(), ArcIntern::from("solved-goto")), 155 | WithSpan::new( 156 | Macro::Builtin(|syntax, mut args, block_id| { 157 | if args.len() != 2 { 158 | return Err(mk_error( 159 | format!("Expected two arguments, found {}", args.len()), 160 | args.span(), 161 | )); 162 | } 163 | 164 | let label = expect_label(args.pop().as_ref().unwrap(), block_id)?; 165 | let register = expect_reg(args.pop().as_ref().unwrap(), syntax, block_id)?; 166 | 167 | Ok(vec![Instruction::Code(Code::Primitive( 168 | Primitive::SolvedGoto { register, label }, 169 | ))]) 170 | }), 171 | dummy_span.clone(), 172 | ), 173 | ); 174 | 175 | macros.insert( 176 | (prelude.to_owned(), ArcIntern::from("input")), 177 | WithSpan::new( 178 | Macro::Builtin(|syntax, mut args, block_id| { 179 | if args.len() != 2 { 180 | return Err(mk_error( 181 | format!("Expected two arguments, found {}", args.len()), 182 | args.span(), 183 | )); 184 | } 185 | 186 | let register = expect_reg(args.pop().as_ref().unwrap(), syntax, block_id)?; 187 | 188 | let second_arg = args.pop().unwrap(); 189 | let span = second_arg.span().to_owned(); 190 | let message = match second_arg.into_inner() { 191 | Value::Ident(raw_message) => { 192 | WithSpan::new(raw_message.trim_matches('"').to_owned(), span) 193 | } 194 | _ => { 195 | return Err(mk_error("Expected a message", span)); 196 | } 197 | }; 198 | 199 | Ok(vec![Instruction::Code(Code::Primitive(Primitive::Input { 200 | register, 201 | message, 202 | }))]) 203 | }), 204 | dummy_span.clone(), 205 | ), 206 | ); 207 | 208 | macros.insert( 209 | (prelude.to_owned(), ArcIntern::from("halt")), 210 | WithSpan::new( 211 | Macro::Builtin(|syntax, args, block_id| { 212 | let (register, message) = print_like(syntax, args, block_id)?; 213 | 214 | Ok(vec![Instruction::Code(Code::Primitive(Primitive::Halt { 215 | register, 216 | message, 217 | }))]) 218 | }), 219 | dummy_span.clone(), 220 | ), 221 | ); 222 | 223 | macros.insert( 224 | (prelude.to_owned(), ArcIntern::from("print")), 225 | WithSpan::new( 226 | Macro::Builtin(|syntax, args, block_id| { 227 | let (register, message) = print_like(syntax, args, block_id)?; 228 | 229 | Ok(vec![Instruction::Code(Code::Primitive(Primitive::Print { 230 | register, 231 | message, 232 | }))]) 233 | }), 234 | dummy_span.clone(), 235 | ), 236 | ); 237 | 238 | macros 239 | } 240 | -------------------------------------------------------------------------------- /src/compiler/src/lua.rs: -------------------------------------------------------------------------------- 1 | use mlua::{AnyUserData, IntoLua, Lua, UserDataMethods, UserDataRegistry, Value}; 2 | use qter_core::{I, Int}; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct LuaMacros { 6 | lua_vm: Lua, 7 | } 8 | 9 | impl LuaMacros { 10 | pub fn new() -> mlua::Result { 11 | let lua_vm = Lua::new(); 12 | 13 | lua_vm.register_userdata_type(Self::init_userdata)?; 14 | 15 | let to_big = 16 | lua_vm.create_function(|_, v| Ok(AnyUserData::wrap(Self::value_to_int(v)?)))?; 17 | 18 | lua_vm.globals().set("big", to_big)?; 19 | 20 | Ok(LuaMacros { lua_vm }) 21 | } 22 | 23 | pub fn add_code(&self, code: &str) -> mlua::Result<()> { 24 | self.lua_vm.load(code).exec() 25 | } 26 | 27 | fn value_to_int(v: Value) -> mlua::Result> { 28 | match v { 29 | Value::Integer(int) => Ok(Int::from(int)), 30 | Value::UserData(data) => data.borrow::>().map(|v| *v), 31 | _ => Err(mlua::Error::runtime("The value isn't an integer!")), 32 | } 33 | } 34 | 35 | fn init_userdata(registry: &mut UserDataRegistry>) { 36 | registry.add_meta_function("__add", |_, (lhs, rhs): (Value, Value)| { 37 | Ok(AnyUserData::wrap( 38 | Self::value_to_int(lhs)? + Self::value_to_int(rhs)?, 39 | )) 40 | }); 41 | 42 | registry.add_meta_function("__sub", |_, (lhs, rhs): (Value, Value)| { 43 | Ok(AnyUserData::wrap( 44 | Self::value_to_int(lhs)? - Self::value_to_int(rhs)?, 45 | )) 46 | }); 47 | 48 | registry.add_meta_function("__mul", |_, (lhs, rhs): (Value, Value)| { 49 | Ok(AnyUserData::wrap( 50 | Self::value_to_int(lhs)? * Self::value_to_int(rhs)?, 51 | )) 52 | }); 53 | 54 | registry.add_meta_function("__div", |_, (lhs, rhs): (Value, Value)| { 55 | Ok(AnyUserData::wrap( 56 | Self::value_to_int(lhs)? / Self::value_to_int(rhs)?, 57 | )) 58 | }); 59 | 60 | registry.add_meta_function("__mod", |_, (lhs, rhs): (Value, Value)| { 61 | Ok(AnyUserData::wrap(Int::::from( 62 | Self::value_to_int(lhs)? % Self::value_to_int(rhs)?, 63 | ))) 64 | }); 65 | 66 | registry.add_meta_function("__unm", |_, v: Value| { 67 | Ok(AnyUserData::wrap(-Self::value_to_int(v)?)) 68 | }); 69 | 70 | registry.add_meta_function("__eq", |_, (lhs, rhs): (Value, Value)| { 71 | Ok(Value::Boolean( 72 | Self::value_to_int(lhs)? == Self::value_to_int(rhs)?, 73 | )) 74 | }); 75 | 76 | registry.add_meta_function("__lt", |_, (lhs, rhs): (Value, Value)| { 77 | Ok(Value::Boolean( 78 | Self::value_to_int(lhs)? < Self::value_to_int(rhs)?, 79 | )) 80 | }); 81 | 82 | registry.add_meta_function("__le", |_, (lhs, rhs): (Value, Value)| { 83 | Ok(Value::Boolean( 84 | Self::value_to_int(lhs)? <= Self::value_to_int(rhs)?, 85 | )) 86 | }); 87 | 88 | registry.add_meta_function("__tostring", |lua_vm, v: Value| { 89 | mlua::String::wrap(Self::value_to_int(v)?.to_string().as_bytes()).into_lua(lua_vm) 90 | }); 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use mlua::{AnyUserData, Function}; 97 | use qter_core::{I, Int}; 98 | 99 | use super::LuaMacros; 100 | 101 | #[test] 102 | fn custom_numeric() { 103 | let lua_vm = LuaMacros::new().unwrap(); 104 | 105 | lua_vm 106 | .add_code( 107 | " 108 | function fail() 109 | assert(false) 110 | end 111 | 112 | function test(zero, too_big, tenth_too_big) 113 | assert(zero < big(10)) 114 | assert(zero + 10 <= big(10)) 115 | assert(too_big / 10 == tenth_too_big) 116 | assert(too_big % 9 == big(1)) 117 | assert(10 / big(6) == big(1)) 118 | assert(10 - big(4) == big(6)) 119 | assert(-big(10) == big(-10)) 120 | end 121 | ", 122 | ) 123 | .unwrap(); 124 | 125 | assert!( 126 | lua_vm 127 | .lua_vm 128 | .globals() 129 | .get::("fail") 130 | .unwrap() 131 | .call::<()>(()) 132 | .is_err() 133 | ); 134 | 135 | let too_big = Int::::from(u64::MAX - 5); 136 | let too_big = too_big * too_big; 137 | 138 | lua_vm 139 | .lua_vm 140 | .globals() 141 | .get::("test") 142 | .unwrap() 143 | .call::<()>(( 144 | AnyUserData::wrap(Int::::zero()), 145 | AnyUserData::wrap(too_big), 146 | AnyUserData::wrap(too_big / Int::::from(10)), 147 | )) 148 | .unwrap(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/compiler/src/macro_expansion.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::OnceCell, mem}; 2 | 3 | use internment::ArcIntern; 4 | use itertools::Itertools; 5 | use pest::error::Error; 6 | use qter_core::{WithSpan, mk_error}; 7 | 8 | use crate::{ 9 | BlockID, Code, ExpandedCode, ExpandedCodeComponent, ExpansionInfo, Instruction, Macro, 10 | ParsedSyntax, TaggedInstruction, 11 | }; 12 | 13 | use super::parsing::Rule; 14 | 15 | pub fn expand(mut parsed: ParsedSyntax) -> Result>> { 16 | // TODO: Logic of `after` 17 | while expand_block(BlockID(0), &mut parsed.expansion_info, &mut parsed.code)? {} 18 | 19 | Ok(ExpandedCode { 20 | block_info: parsed.expansion_info.block_info, 21 | expanded_code_components: parsed 22 | .code 23 | .into_iter() 24 | .map(|tagged_instruction| { 25 | let span = tagged_instruction.span().to_owned(); 26 | let (instruction, maybe_block_id) = tagged_instruction.into_inner(); 27 | 28 | let expanded = match instruction { 29 | Instruction::Label(label) => ExpandedCodeComponent::Label(label), 30 | Instruction::Code(Code::Primitive(primitive)) => { 31 | ExpandedCodeComponent::Instruction( 32 | Box::new(primitive), 33 | maybe_block_id.unwrap(), 34 | ) 35 | } 36 | illegal => unreachable!("{illegal:?}"), 37 | }; 38 | 39 | WithSpan::new(expanded, span) 40 | }) 41 | .collect_vec(), 42 | }) 43 | } 44 | 45 | /// Returns whether any changes were made 46 | fn expand_block( 47 | block_id: BlockID, 48 | expansion_info: &mut ExpansionInfo, 49 | code: &mut Vec>, 50 | ) -> Result>> { 51 | // Will be set if anything is ever changed 52 | let changed = OnceCell::<()>::new(); 53 | 54 | *code = mem::take(code) 55 | .into_iter() 56 | .map(|mut tagged_instruction| { 57 | let maybe_block_id = &mut tagged_instruction.1; 58 | if maybe_block_id.is_none() { 59 | *maybe_block_id = Some(block_id); 60 | let _ = changed.set(()); 61 | } 62 | 63 | tagged_instruction 64 | }) 65 | .flat_map(|tagged_instruction| { 66 | let span = tagged_instruction.span().to_owned(); 67 | 68 | let (instruction, maybe_block_id) = tagged_instruction.into_inner(); 69 | let block_id = maybe_block_id.unwrap(); 70 | 71 | let block_info = expansion_info.block_info.0.get_mut(&block_id).unwrap(); 72 | 73 | match instruction { 74 | Instruction::Label(mut label) => { 75 | if label.maybe_block_id.is_none() { 76 | label.maybe_block_id = Some(block_id); 77 | let _ = changed.set(()); 78 | } 79 | 80 | block_info.labels.push(label.clone()); 81 | 82 | vec![Ok(WithSpan::new( 83 | (Instruction::Label(label), maybe_block_id), 84 | span, 85 | ))] 86 | } 87 | Instruction::Define(define) => { 88 | for found_define in &block_info.defines { 89 | if *found_define.name == *define.name { 90 | return vec![Err(mk_error( 91 | "Cannot shadow a `.define` in the same scope!", 92 | define.name.span(), 93 | ))]; 94 | } 95 | } 96 | 97 | block_info.defines.push(define); 98 | let _ = changed.set(()); 99 | 100 | vec![] 101 | } 102 | Instruction::Registers(decl) => { 103 | if block_info.registers.is_some() { 104 | vec![Err(mk_error( 105 | "Cannot have multiple register declarations in the same scope!", 106 | span, 107 | ))] 108 | } else { 109 | block_info.registers = Some(decl); 110 | let _ = changed.set(()); 111 | vec![] 112 | } 113 | } 114 | Instruction::Code(code) => { 115 | match expand_code(block_id, expansion_info, code, &changed) { 116 | Ok(tagged_instructions) => tagged_instructions 117 | .into_iter() 118 | .map(|tagged_instruction| { 119 | Ok(WithSpan::new(tagged_instruction, span.clone())) 120 | }) 121 | .collect_vec(), 122 | Err(e) => vec![Err(e)], 123 | } 124 | } 125 | Instruction::Constant(_) => todo!(), 126 | Instruction::LuaCall(_) => todo!(), 127 | } 128 | }) 129 | .collect::>()?; 130 | 131 | Ok(changed.get().is_some()) 132 | } 133 | 134 | fn expand_code( 135 | block_id: BlockID, 136 | expansion_info: &mut ExpansionInfo, 137 | code: Code, 138 | changed: &OnceCell<()>, 139 | ) -> Result, Box>> { 140 | let macro_call = match code { 141 | Code::Primitive(prim) => { 142 | return Ok(vec![( 143 | Instruction::Code(Code::Primitive(prim)), 144 | Some(block_id), 145 | )]); 146 | } 147 | Code::Macro(mac) => mac, 148 | }; 149 | 150 | let _ = changed.set(()); 151 | 152 | let Some(macro_access) = expansion_info.available_macros.get(&( 153 | macro_call.name.span().source(), 154 | ArcIntern::clone(&*macro_call.name), 155 | )) else { 156 | return Err(mk_error( 157 | "Macro was not found in this scope", 158 | macro_call.name.span(), 159 | )); 160 | }; 161 | 162 | let macro_def = expansion_info 163 | .macros 164 | .get(&( 165 | ArcIntern::clone(macro_access), 166 | ArcIntern::clone(¯o_call.name), 167 | )) 168 | .unwrap(); 169 | 170 | Ok(match &**macro_def { 171 | Macro::UserDefined { 172 | branches: _, 173 | after: _, 174 | } => todo!(), 175 | Macro::Builtin(macro_fn) => macro_fn(expansion_info, macro_call.arguments, block_id)? 176 | .into_iter() 177 | .map(|instruction| (instruction, Some(block_id))) 178 | .collect_vec(), 179 | }) 180 | } 181 | 182 | #[cfg(test)] 183 | mod tests { 184 | use crate::{macro_expansion::expand, parsing::parse}; 185 | 186 | #[test] 187 | fn bruh() { 188 | let code = " 189 | .registers { 190 | a, b ← 3x3 builtin (90, 90) 191 | } 192 | 193 | loop: 194 | add a 1 195 | print \"What da heck\" a 196 | solved-goto a loop 197 | 198 | add b 89 199 | solved-goto b over 200 | goto loop 201 | 202 | over: 203 | 204 | halt \"Poggers\" b 205 | "; 206 | 207 | let parsed = match parse(code, &|_| unreachable!(), false) { 208 | Ok(v) => v, 209 | Err(e) => panic!("{e}"), 210 | }; 211 | 212 | let expanded = match expand(parsed) { 213 | Ok(v) => v, 214 | Err(e) => panic!("{e}"), 215 | }; 216 | 217 | println!("{expanded:?}"); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/compiler/tests/average/average.q: -------------------------------------------------------------------------------- 1 | Puzzles 2 | A: 3x3 3 | 4 | 1 | input "First number" 5 | R' F' L U' L U L F U' R 6 | max-input 90 7 | 2 | input "Second number" 8 | U F R' D' R2 F R' U' D 9 | max-input 90 10 | 3 | B2 R L2 D L' F' D2 F' L2 11 | B' U' R D' L' B2 R F 12 | 4 | solved-goto DFR FR 6 13 | 5 | goto 3 14 | 6 | R' F' L U' L U L F U' R 15 | 7 | R' U F' L' U' L' U L' F R 16 | 8 | solved-goto ULF UL 13 17 | 9 | R' U F' L' U' L' U L' F R 18 | 10 | solved-goto ULF UL 13 19 | 11 | U F R' D' R2 F R' U' D 20 | 12 | goto 7 21 | 13 | halt until DFR FR solved 22 | "The average is" 23 | D' U R F' R2 D R F' U' 24 | -------------------------------------------------------------------------------- /src/compiler/tests/average/average_transform.qat: -------------------------------------------------------------------------------- 1 | .registers { 2 | A, B <- 3x3 builtin (90, 90) 3 | } 4 | 5 | -- Calculate the average of two numbers 6 | input "First number:" A 7 | input "Second number:" B 8 | print "Calculating average..." 9 | sum_loop: 10 | add A 1 11 | add B 89 12 | solved-goto B found_sum 13 | goto sum_loop 14 | found_sum: 15 | add A 1 16 | divide_by_2: 17 | add A 89 18 | solved-goto A stop 19 | add A 89 20 | solved-goto A stop 21 | add B 1 22 | goto divide_by_2 23 | stop: 24 | halt "The average is" B 25 | -------------------------------------------------------------------------------- /src/compiler/tests/fib/fib.q: -------------------------------------------------------------------------------- 1 | Puzzles 2 | A: 3x3 3 | 4 | 1 | input "Which Fibonacci number to calculate:" 5 | B2 U2 L F' R B L2 D2 B R' F L 6 | max-input 8 7 | 2 | solved-goto UFR 14 8 | 3 | D L' F L2 B L' F' L B' D' L' 9 | 4 | L' F' R B' D2 L2 B' R' F L' U2 B2 10 | 5 | solved-goto UFR 15 11 | 6 | repeat until DL DFL solved 12 | L U' B R' L B' L' U' 13 | L U R2 B R2 D2 R2 D' 14 | 7 | L' F' R B' D2 L2 B' R' F L' U2 B2 15 | 8 | solved-goto UFR 16 16 | 9 | repeat until FR DRF solved 17 | D' B' U2 B D' F' D L' D2 18 | F' R' D2 F2 R F2 R2 U' R' 19 | 10 | L' F' R B' D2 L2 B' R' F L' U2 B2 20 | 11 | solved-goto UFR 17 21 | 12 | repeat until UF solved 22 | B R2 D' R B D F2 U2 D' 23 | F' L2 F D2 F B2 D' L' U' 24 | 13 | goto 4 25 | 14 | halt "The number is: 0" 26 | 15 | halt until DL DFL solved 27 | "The number is" 28 | L D B L' F L B' L2 F' L D' 29 | 16 | halt until FR DRF solved 30 | "The number is" 31 | F2 L2 U2 D' R U' B L' B L' U' 32 | 17 | halt until UF solved 33 | "The number is" 34 | U L' R' F' U' F' L' F2 L U R 35 | -------------------------------------------------------------------------------- /src/compiler/tests/fib/fib.qat: -------------------------------------------------------------------------------- 1 | .registers { 2 | A, B, C, D <- 3x3 builtin (30, 18, 10, 9) 3 | } 4 | 5 | .macro fib-shuffle { 6 | ($R1:reg $R2:reg $R3:reg $counter:reg) => { 7 | dec $counter 8 | if solved $counter { 9 | halt "The number is" $R1 10 | } 11 | while not-solved $R1 { 12 | dec $R1 13 | inc $R2 14 | inc $R3 15 | } 16 | } 17 | } 18 | 19 | input "Which Fibonacci number to calculate:" D 20 | if solved D { 21 | halt "The number is: 0" 22 | } 23 | inc B 24 | loop { 25 | fib-shuffle B A C D 26 | fib-shuffle A C B D 27 | fib-shuffle C B A D 28 | } 29 | 30 | --[[ 31 | A B C D 32 | 0 1 0 8 33 | 1 0 1 7 34 | 0 1 2 6 35 | 2 3 0 5 36 | 5 0 3 4 37 | 0 5 8 3 38 | 8 13 0 2 39 | 21 0 3 1 40 | --]] -------------------------------------------------------------------------------- /src/compiler/tests/fib/fib_transform.qat: -------------------------------------------------------------------------------- 1 | .registers { 2 | A, B, C, D <- 3x3 builtin (30, 18, 10, 9) 3 | } 4 | 5 | input "Which Fibonacci number to calculate:" D 6 | solved-goto D do_if_1 7 | goto after_if_1 8 | do_if_1: 9 | halt "The number is: 0" 10 | after_if_1: 11 | add B 1 12 | continue_1: 13 | add D 8 14 | solved-goto D do_if_2 15 | goto after_if_2 16 | do_if_2: 17 | halt "The number is" B 18 | after_if_2: 19 | continue_2: 20 | solved-goto B break_2 21 | add B 17 22 | add A 1 23 | add C 1 24 | goto continue_2 25 | break_2: 26 | add D 8 27 | solved-goto D do_if_3 28 | goto after_if_3 29 | do_if_3: 30 | halt "The number is" A 31 | after_if_3: 32 | continue_3: 33 | solved-goto A break_3 34 | add A 29 35 | add C 1 36 | add B 1 37 | goto continue_3 38 | break_3: 39 | add D 8 40 | solved-goto D do_if_4 41 | goto after_if_4 42 | do_if_4: 43 | halt "The number is" C 44 | after_if_4: 45 | continue_4: 46 | solved-goto C continue_1 47 | add C 9 48 | add B 1 49 | add A 1 50 | goto continue_4 51 | -------------------------------------------------------------------------------- /src/compiler/tests/multiply/multiply.c: -------------------------------------------------------------------------------- 1 | /** 2 | * A C stub for a QAT multiplication program. The architecture of the program is 3 | * assumed to be 30/30/30, not working otherwise. 4 | * 5 | * Input: 6 | * - Argument 1: First number (0-29) 7 | * - Argument 2: Second number (0-29) 8 | * - Argument 3: 0 9 | * 10 | * Output: 11 | * - Argument 1: Result of multiplication modulo 30 12 | * - Argument 2: 0 13 | * - Argument 3: 0 14 | * 15 | * Caveats: The program is faster when the first argument is larger than the 16 | * second. 17 | */ 18 | 19 | #include 20 | #include 21 | 22 | #define solved_goto(a, label) \ 23 | if (*a == 0) \ 24 | { \ 25 | goto label; \ 26 | } 27 | 28 | #define add(a, n) \ 29 | { \ 30 | *a += n; \ 31 | *a = (*a % 30 + 30) % 30; \ 32 | } 33 | 34 | void multiply(int *a, int *b, int *c) 35 | { 36 | solved_goto(a, l1); 37 | solved_goto(b, l2); 38 | l3: 39 | solved_goto(b % 10, l4); 40 | add(b, -1); 41 | add(c, 1); 42 | solved_goto(b % 10, l5); 43 | add(b, -1); 44 | add(c, 1); 45 | goto l3; 46 | l4: 47 | solved_goto(c % 3, l6); 48 | add(c, -1); 49 | add(b, 1); 50 | goto l4; 51 | l6: 52 | solved_goto(c, l7); 53 | add(c, -3); 54 | add(b, 3); 55 | goto l6; 56 | l7: 57 | solved_goto(b, l8); 58 | add(b, -2); 59 | add(c, 1); 60 | goto l7; 61 | l8: 62 | solved_goto(c % 3, l9); 63 | add(c, -1); 64 | add(b, 1); 65 | goto l8; 66 | l9: 67 | solved_goto(c, l10); 68 | add(c, -3); 69 | add(b, 3); 70 | goto l9; 71 | l10: 72 | solved_goto(a, l11); 73 | add(a, -1); 74 | add(c, 2); 75 | goto l10; 76 | l11: 77 | solved_goto(c % 10, l12); 78 | add(c, -1); 79 | add(a, 1); 80 | goto l11; 81 | l12: 82 | solved_goto(c, l3); 83 | add(c, -10); 84 | add(a, 10); 85 | goto l12; 86 | l13: 87 | solved_goto(b, l15); 88 | add(b, -3); 89 | add(c, 1); 90 | goto l13; 91 | l15: 92 | solved_goto(c % 3, l16); 93 | add(c, -1); 94 | add(b, 1); 95 | goto l15; 96 | l16: 97 | solved_goto(c, l17); 98 | add(c, -3); 99 | add(b, 3); 100 | goto l16; 101 | l17: 102 | solved_goto(a, l18); 103 | add(a, -1); 104 | add(c, 3); 105 | goto l17; 106 | l18: 107 | solved_goto(c % 10, l19); 108 | add(c, -1); 109 | add(a, 1); 110 | goto l18; 111 | l19: 112 | solved_goto(c, l20); 113 | add(c, -10); 114 | add(a, 10); 115 | goto l19; 116 | l5: 117 | solved_goto(c % 3, l21); 118 | add(c, -1); 119 | add(b, 1); 120 | goto l5; 121 | l21: 122 | solved_goto(c, l20); 123 | add(c, -3); 124 | add(b, 3); 125 | goto l21; 126 | l20: 127 | solved_goto(b % 3, l13); 128 | add(b, -1); 129 | solved_goto(b, l42); 130 | goto l49; 131 | l22: 132 | solved_goto(b % 10, l23); 133 | add(b, -1); 134 | l49: 135 | add(c, 1); 136 | solved_goto(b % 10, l24); 137 | add(b, -1); 138 | add(c, 1); 139 | solved_goto(b % 10, l24); 140 | add(b, -1); 141 | add(c, 1); 142 | solved_goto(b % 10, l24); 143 | add(b, -1); 144 | add(c, 1); 145 | solved_goto(b % 10, l24); 146 | add(b, -1); 147 | add(c, 1); 148 | goto l22; 149 | l23: 150 | solved_goto(c % 3, l25); 151 | add(c, -1); 152 | add(b, 1); 153 | goto l23; 154 | l25: 155 | solved_goto(c, l26); 156 | add(c, -3); 157 | add(b, 3); 158 | goto l25; 159 | l26: 160 | solved_goto(b, l27); 161 | add(b, -5); 162 | add(c, 1); 163 | goto l26; 164 | l27: 165 | solved_goto(c % 3, l28); 166 | add(c, -1); 167 | add(b, 1); 168 | goto l27; 169 | l28: 170 | solved_goto(c, l29); 171 | add(c, -3); 172 | add(b, 3); 173 | goto l28; 174 | l29: 175 | solved_goto(a, l30); 176 | add(a, -1); 177 | add(c, 5); 178 | goto l29; 179 | l30: 180 | solved_goto(c % 10, l31); 181 | add(c, -1); 182 | add(a, 1); 183 | goto l30; 184 | l31: 185 | solved_goto(c, l22); 186 | add(c, -10); 187 | add(a, 10); 188 | goto l31; 189 | l24: 190 | solved_goto(c % 3, l32); 191 | add(c, -1); 192 | add(b, 1); 193 | goto l24; 194 | l32: 195 | solved_goto(c, l33); 196 | add(c, -3); 197 | add(b, 3); 198 | goto l32; 199 | l33: 200 | add(b, -1); 201 | solved_goto(b % 10, l34); 202 | add(b, 1); 203 | l35: 204 | solved_goto(b, l36); 205 | add(b, -7); 206 | add(c, 1); 207 | goto l35; 208 | l36: 209 | solved_goto(c % 3, l37); 210 | add(c, -1); 211 | add(b, 1); 212 | goto l36; 213 | l37: 214 | solved_goto(c, l38); 215 | add(c, -3); 216 | add(b, 3); 217 | goto l37; 218 | l38: 219 | solved_goto(a, l39); 220 | add(a, -1); 221 | add(c, 7); 222 | goto l38; 223 | l39: 224 | solved_goto(c % 10, l40); 225 | add(c, -1); 226 | add(a, 1); 227 | goto l39; 228 | l40: 229 | solved_goto(c, l33); 230 | add(c, -10); 231 | add(a, 10); 232 | goto l40; 233 | l41: 234 | add(b, -1); 235 | l34: 236 | solved_goto(b, l42); 237 | add(b, 1); 238 | l43: 239 | solved_goto(b, l44); 240 | add(b, -11); 241 | add(c, 1); 242 | goto l43; 243 | l44: 244 | solved_goto(c % 3, l45); 245 | add(c, -1); 246 | add(b, 1); 247 | goto l44; 248 | l45: 249 | solved_goto(c, l46); 250 | add(c, -3); 251 | add(b, 3); 252 | goto l45; 253 | l46: 254 | solved_goto(a, l47); 255 | add(a, -1); 256 | add(c, 11); 257 | goto l46; 258 | l47: 259 | solved_goto(c % 10, l48); 260 | add(c, -1); 261 | add(a, 1); 262 | goto l47; 263 | l48: 264 | solved_goto(c, l41); 265 | add(c, -10); 266 | add(a, 10); 267 | goto l48; 268 | l1: 269 | solved_goto(b, l42); 270 | add(b, -1); 271 | goto l1; 272 | l2: 273 | solved_goto(a, l42); 274 | add(a, -1); 275 | goto l2; 276 | l42: 277 | return; 278 | } 279 | 280 | int main() 281 | { 282 | for (int i = 0; i < 30; i++) 283 | { 284 | for (int j = 0; j < 30; j++) 285 | { 286 | int a = i; 287 | int b = j; 288 | int c = 0; 289 | multiply(&a, &b, &c); 290 | printf("%d * %d = %d\n", i, j, a); 291 | assert((i * j) % 30 == a); 292 | assert(b == 0); 293 | assert(c == 0); 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/compiler/tests/multiply/multiply.q: -------------------------------------------------------------------------------- 1 | Puzzles 2 | A: 3x3 3 | 4 | 1 | input "Enter number X" 5 | L2 F2 U L' F D' F' U' L' F U D L' U' 6 | max-input 29 7 | 2 | input "Enter number Y" 8 | R2 L U' R' L2 F' D R' D L B2 D2 9 | max-input 29 10 | 3 | solved-goto FR UFR 75 11 | 4 | solved-goto UB DLB 77 12 | 5 | solved-goto UB 10 13 | 6 | F2 B2 U F2 B2 D' B L2 D2 B' D B2 D L D2 B2 D' 14 | 7 | solved-goto UB 26 15 | 8 | F2 B2 U F2 B2 D' B L2 D2 B' D B2 D L D2 B2 D' 16 | 9 | goto 5 17 | 10 | repeat until ULF solved 18 | D B2 D2 L' D' B2 D' B D2 19 | L2 B' D F2 B2 U' F2 B2 20 | 11 | repeat until UR ULF solved 21 | D' R L' U' F' B2 L B 22 | U B L U R' D2 B' U' 23 | 12 | repeat until UB DLB solved 24 | F' B' D2 R' B2 R U R2 B2 25 | L' B' U B R2 L2 F R L' 26 | 13 | repeat until ULF solved 27 | D B2 D2 L' D' B2 D' B D2 28 | L2 B' D F2 B2 U' F2 B2 29 | 14 | repeat until UR ULF solved 30 | D' R L' U' F' B2 L B 31 | U B L U R' D2 B' U' 32 | 15 | B2 L 33 | 16 | repeat until FR UFR solved 34 | F D R2 F R' U F' 35 | R2 F D2 L2 D L2 36 | 17 | L' B2 D2 37 | 18 | repeat until UR solved 38 | L D' F' D' R D' R U2 39 | B R B2 U R' U F D' 40 | 19 | D2 41 | 20 | repeat until UR ULF solved 42 | F2 L' B' D F2 U' R2 F 43 | U2 R' D' B U2 F' L2 U 44 | 21 | goto 5 45 | 22 | repeat until UB DLB solved 46 | U' R2 L2 U R2 F2 D2 R' 47 | F2 L' U2 L U L' B D' B 48 | 23 | repeat until ULF solved 49 | D B2 D2 L' D' B2 D' B D2 50 | L2 B' D F2 B2 U' F2 B2 51 | 24 | repeat until UR ULF solved 52 | D' R L' U' F' B2 L B 53 | U B L U R' D2 B' U' 54 | 25 | repeat until FR UFR solved 55 | U' R' L2 B' L' D' F' R 56 | F' D R' L B2 R2 L2 U' R2 57 | 26 | D2 58 | 27 | repeat until UR solved 59 | L D' F' D' R D' R U2 60 | B R B2 U R' U F D' 61 | 28 | D2 62 | 29 | repeat until UR ULF solved 63 | F2 L' B' D F2 U' R2 F 64 | U2 R' D' B U2 F' L2 U 65 | 30 | goto 33 66 | 31 | repeat until ULF solved 67 | D B2 D2 L' D' B2 D' B D2 68 | L2 B' D F2 B2 U' F2 B2 69 | 32 | repeat until UR ULF solved 70 | D' R L' U' F' B2 L B 71 | U B L U R' D2 B' U' 72 | 33 | solved-goto DLB 22 73 | 34 | D2 B2 L' D' R D' F R L2 U R2 L' 74 | 35 | solved-goto UB DLB 89 75 | 36 | goto 39 76 | 37 | solved-goto UB 49 77 | 38 | D2 B2 L' D' R D' F R L2 U R2 L' 78 | 39 | U L2 B' L U' B' U2 R B' R' B L 79 | 40 | solved-goto UB 60 80 | 41 | F2 B2 U F2 B2 D' B L2 D2 B' D B2 D L D2 B2 D' 81 | 42 | solved-goto UB 60 82 | 43 | F2 B2 U F2 B2 D' B L2 D2 B' D B2 D L D2 B2 D' 83 | 44 | solved-goto UB 60 84 | 45 | F2 B2 U F2 B2 D' B L2 D2 B' D B2 D L D2 B2 D' 85 | 46 | solved-goto UB 60 86 | 47 | F2 B2 U F2 B2 D' B L2 D2 B' D B2 D L D2 B2 D' 87 | 48 | goto 37 88 | 49 | repeat until ULF solved 89 | D B2 D2 L' D' B2 D' B D2 90 | L2 B' D F2 B2 U' F2 B2 91 | 50 | repeat until UR ULF solved 92 | D' R L' U' F' B2 L B 93 | U B L U R' D2 B' U' 94 | 51 | repeat until UB DLB solved 95 | F2 B2 U F2 D L' B D2 B2 96 | D L2 D' B R' L2 B2 R L 97 | 52 | repeat until ULF solved 98 | D B2 D2 L' D' B2 D' B D2 99 | L2 B' D F2 B2 U' F2 B2 100 | 53 | repeat until UR ULF solved 101 | D' R L' U' F' B2 L B 102 | U B L U R' D2 B' U' 103 | 54 | repeat until FR UFR solved 104 | D2 R' F' D2 R F R F' R' 105 | B' D F' L' D' B' L2 U' B2 106 | 55 | D2 107 | 56 | repeat until UR solved 108 | L D' F' D' R D' R U2 109 | B R B2 U R' U F D' 110 | 57 | D2 111 | 58 | repeat until UR ULF solved 112 | F2 L' B' D F2 U' R2 F 113 | U2 R' D' B U2 F' L2 U 114 | 59 | goto 37 115 | 60 | repeat until ULF solved 116 | D B2 D2 L' D' B2 D' B D2 117 | L2 B' D F2 B2 U' F2 B2 118 | 61 | repeat until UR ULF solved 119 | D' R L' U' F' B2 L B 120 | U B L U R' D2 B' U' 121 | 62 | D2 B2 L' D' R D' F R L2 U R2 L' 122 | 63 | solved-goto UB 75 123 | 64 | R2 L U' R' L2 F' D R' D L B2 D2 124 | 65 | repeat until UB DLB solved 125 | F' D' F' U' R B2 U2 D' 126 | R D F2 L B2 L D2 L2 D2 127 | 66 | repeat until ULF solved 128 | D B2 D2 L' D' B2 D' B D2 129 | L2 B' D F2 B2 U' F2 B2 130 | 67 | repeat until UR ULF solved 131 | D' R L' U' F' B2 L B 132 | U B L U R' D2 B' U' 133 | 68 | repeat until FR UFR solved 134 | D2 F U2 R' U D2 F D' 135 | R D R2 D F' R U R 136 | 69 | D2 137 | 70 | repeat until UR solved 138 | L D' F' D' R D' R U2 139 | B R B2 U R' U F D' 140 | 71 | D2 141 | 72 | repeat until UR ULF solved 142 | F2 L' B' D F2 U' R2 F 143 | U2 R' D' B U2 F' L2 U 144 | 73 | goto 62 145 | 74 | D2 B2 L' D' R D' F R L2 U R2 L' 146 | 75 | solved-goto UB DLB 89 147 | 76 | R2 L U' R' L2 F' D R' D L B2 D2 148 | 77 | repeat until UB DLB solved 149 | R' B' R D F L2 U' B2 L2 150 | B' U L2 U L' U' B2 L2 F' 151 | 78 | repeat until ULF solved 152 | D B2 D2 L' D' B2 D' B D2 153 | L2 B' D F2 B2 U' F2 B2 154 | 79 | repeat until UR ULF solved 155 | D' R L' U' F' B2 L B 156 | U B L U R' D2 B' U' 157 | 80 | repeat until FR UFR solved 158 | R' F2 D F' B2 L2 U L2 U 159 | F' B2 R D2 R' D' F2 D' 160 | 81 | D2 161 | 82 | repeat until UR solved 162 | L D' F' D' R D' R U2 163 | B R B2 U R' U F D' 164 | 83 | D2 165 | 84 | repeat until UR ULF solved 166 | F2 L' B' D F2 U' R2 F 167 | U2 R' D' B U2 F' L2 U 168 | 85 | goto 74 169 | 86 | repeat until UB DLB solved 170 | D2 B2 L' D' R D' F R L2 U R2 L' 171 | 87 | goto 89 172 | 88 | repeat until FR UFR solved 173 | U L U' D' F' L U F D F' L U' F2 L2 174 | 89 | halt until FR UFR solved 175 | "(X * Y) mod 30 =" 176 | U L U' D' F' L U F D F' L U' F2 L2 177 | -------------------------------------------------------------------------------- /src/compiler/tests/multiply/multiply_transform.qat: -------------------------------------------------------------------------------- 1 | .registers { 2 | A, B, C <- 3x3 builtin (30, 30, 30) 3 | } 4 | 5 | input "Enter number X" C 6 | input "Enter number Y" B 7 | solved-goto C l1 8 | solved-goto B l2 9 | l3: 10 | solved-goto B%10 l4 11 | add B 29 12 | add A 1 13 | solved-goto B%10 l5 14 | add B 29 15 | add A 1 16 | goto l3 17 | l4: 18 | solved-goto A%3 l6 19 | add A 29 20 | add B 1 21 | goto l4 22 | l6: 23 | solved-goto A l7 24 | add A 27 25 | add B 3 26 | goto l6 27 | l7: 28 | solved-goto B l8 29 | add B 28 30 | add A 1 31 | goto l7 32 | l8: 33 | solved-goto A%3 l9 34 | add A 29 35 | add B 1 36 | goto l8 37 | l9: 38 | solved-goto A l10 39 | add A 27 40 | add B 3 41 | goto l9 42 | l10: 43 | solved-goto C l11 44 | add C 29 45 | add A 2 46 | goto l10 47 | l11: 48 | solved-goto A%10 l12 49 | add A 29 50 | add C 1 51 | goto l11 52 | l12: 53 | solved-goto A l3 54 | add A 20 55 | add C 10 56 | goto l12 57 | l13: 58 | solved-goto B l15 59 | add B 27 60 | add A 1 61 | goto l13 62 | l15: 63 | solved-goto A%3 l16 64 | add A 29 65 | add B 1 66 | goto l15 67 | l16: 68 | solved-goto A l17 69 | add A 27 70 | add B 3 71 | goto l16 72 | l17: 73 | solved-goto C l18 74 | add C 29 75 | add A 3 76 | goto l17 77 | l18: 78 | solved-goto A%10 l19 79 | add A 29 80 | add C 1 81 | goto l18 82 | l19: 83 | solved-goto A l20 84 | add A 20 85 | add C 10 86 | goto l19 87 | l5: 88 | solved-goto A%3 l21 89 | add A 29 90 | add B 1 91 | goto l5 92 | l21: 93 | solved-goto A l20 94 | add A 27 95 | add B 3 96 | goto l21 97 | l20: 98 | solved-goto B%3 l13 99 | add B 29 100 | solved-goto B l42 101 | goto l49 102 | l22: 103 | solved-goto B%10 l23 104 | add B 29 105 | l49: 106 | add A 1 107 | solved-goto B%10 l24 108 | add B 29 109 | add A 1 110 | solved-goto B%10 l24 111 | add B 29 112 | add A 1 113 | solved-goto B%10 l24 114 | add B 29 115 | add A 1 116 | solved-goto B%10 l24 117 | add B 29 118 | add A 1 119 | goto l22 120 | l23: 121 | solved-goto A%3 l25 122 | add A 29 123 | add B 1 124 | goto l23 125 | l25: 126 | solved-goto A l26 127 | add A 27 128 | add B 3 129 | goto l25 130 | l26: 131 | solved-goto B l27 132 | add B 25 133 | add A 1 134 | goto l26 135 | l27: 136 | solved-goto A%3 l28 137 | add A 29 138 | add B 1 139 | goto l27 140 | l28: 141 | solved-goto A l29 142 | add A 27 143 | add B 3 144 | goto l28 145 | l29: 146 | solved-goto C l30 147 | add C 29 148 | add A 5 149 | goto l29 150 | l30: 151 | solved-goto A%10 l31 152 | add A 29 153 | add C 1 154 | goto l30 155 | l31: 156 | solved-goto A l22 157 | add A 20 158 | add C 10 159 | goto l31 160 | l24: 161 | solved-goto A%3 l32 162 | add A 29 163 | add B 1 164 | goto l24 165 | l32: 166 | solved-goto A l33 167 | add A 27 168 | add B 3 169 | goto l32 170 | l33: 171 | add B 29 172 | solved-goto B%10 l34 173 | add B 1 174 | l35: 175 | solved-goto B l36 176 | add B 23 177 | add A 1 178 | goto l35 179 | l36: 180 | solved-goto A%3 l37 181 | add A 29 182 | add B 1 183 | goto l36 184 | l37: 185 | solved-goto A l38 186 | add A 27 187 | add B 3 188 | goto l37 189 | l38: 190 | solved-goto C l39 191 | add C 29 192 | add A 7 193 | goto l38 194 | l39: 195 | solved-goto A%10 l40 196 | add A 29 197 | add C 1 198 | goto l39 199 | l40: 200 | solved-goto A l33 201 | add A 20 202 | add C 10 203 | goto l40 204 | l41: 205 | add B 29 206 | l34: 207 | solved-goto B l42 208 | add B 1 209 | l43: 210 | solved-goto B l44 211 | add B 19 212 | add A 1 213 | goto l43 214 | l44: 215 | solved-goto A%3 l45 216 | add A 29 217 | add B 1 218 | goto l44 219 | l45: 220 | solved-goto A l46 221 | add A 27 222 | add B 3 223 | goto l45 224 | l46: 225 | solved-goto C l47 226 | add C 29 227 | add A 11 228 | goto l46 229 | l47: 230 | solved-goto A%10 l48 231 | add A 29 232 | add C 1 233 | goto l47 234 | l48: 235 | solved-goto A l41 236 | add A 20 237 | add C 10 238 | goto l48 239 | l1: 240 | solved-goto B l42 241 | add B 29 242 | goto l1 243 | l2: 244 | solved-goto C l42 245 | add C 29 246 | goto l2 247 | l42: 248 | halt "(X * Y) mod 30 =" C 249 | -------------------------------------------------------------------------------- /src/compiler/tests/simple/simple.q: -------------------------------------------------------------------------------- 1 | Puzzles 2 | A: 3x3 3 | 4 | 1 | input "First number:" U max-input 3 5 | 2 | input "Second number:" U max-input 3 6 | 3 | halt until UF solved U' 7 | -------------------------------------------------------------------------------- /src/compiler/tests/simple/simple.qat: -------------------------------------------------------------------------------- 1 | .registers { 2 | A <- 3x3 (U) 3 | } 4 | 5 | input "First number:" A 6 | input "Second number:" A 7 | halt "(A + B) % 4 =" A 8 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-Ctarget-cpu=native"] -------------------------------------------------------------------------------- /src/cycle_combination_solver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cycle_combination_solver" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | fastrand = "2.3.0" 8 | fxhash = "0.2.1" 9 | integer-partitions = "0.1.1" 10 | itertools = "0.14.0" 11 | memoize = "0.4.2" 12 | num-traits = "0.2.19" 13 | pareto_front = "1.0.1" 14 | puzzle_geometry = { version = "0.1.0", path = "../puzzle_geometry" } 15 | qter_core = { version = "0.1.0", path = "../qter_core" } 16 | thiserror = "2.0.11" 17 | 18 | [build-dependencies] 19 | cfg_aliases = "0.2.1" 20 | 21 | [lints] 22 | workspace = true 23 | 24 | # [profile.release] 25 | # debug = true 26 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/build.rs: -------------------------------------------------------------------------------- 1 | use cfg_aliases::cfg_aliases; 2 | 3 | fn main() { 4 | cfg_aliases! { 5 | simd8: { 6 | all( 7 | any( 8 | target_arch = "aarch64", 9 | target_arch = "arm64ec", 10 | all( 11 | target_arch = "arm", 12 | target_feature = "v7" 13 | ) 14 | ), 15 | target_feature = "neon", 16 | target_endian = "little" 17 | ) 18 | }, 19 | simd16: { 20 | any( 21 | target_feature = "ssse3", 22 | target_feature = "simd128", 23 | all( 24 | any( 25 | target_arch = "aarch64", 26 | target_arch = "arm64ec" 27 | ), 28 | target_feature = "neon", 29 | target_endian = "little" 30 | ), 31 | all( 32 | target_arch = "arm", 33 | target_feature = "v7", 34 | target_feature = "neon", 35 | target_endian = "little" 36 | ) 37 | ) 38 | }, 39 | simd8and16: { 40 | all( 41 | simd8, 42 | simd16 43 | ) 44 | }, 45 | // simd8and16: { not(l) }, // true 46 | // simd8and16: { l }, // false 47 | avx2: { 48 | target_feature = "avx2" 49 | }, 50 | // avx2: { not(l) }, // true 51 | // avx2: { l }, // false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(test, slice_index_methods, portable_simd, abi_vectorcall)] 2 | #![warn(clippy::pedantic)] 3 | #![allow(clippy::similar_names, clippy::too_many_lines)] 4 | 5 | mod phase1; 6 | pub mod phase2; 7 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase1/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase1/best_lcm_testing.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from timeit import default_timer 3 | import heapq 4 | import math 5 | import operator 6 | 7 | 8 | def p_adic_valuation(n, p): 9 | exponent = 0 10 | while n % p == 0 and n != 0: 11 | n //= p 12 | exponent += 1 13 | return exponent 14 | 15 | 16 | @functools.cache 17 | def integer_partitions(n): 18 | if n == 0: 19 | return {()} 20 | answer = {(n,)} 21 | for x in range(1, n): 22 | for y in integer_partitions(n - x): 23 | answer.add(tuple(sorted((x,) + y))) 24 | return answer 25 | 26 | 27 | def partition_order(partition, orientation_count): 28 | lcm = math.lcm(*partition) 29 | if orientation_count == 1: 30 | return lcm 31 | order = lcm 32 | 33 | always_orient = None 34 | critical_orient = None 35 | max_p_adic_valuation = -1 36 | 37 | for j, permutation_order in enumerate(partition): 38 | curr_p_adic_valuation = p_adic_valuation( 39 | permutation_order, 40 | orientation_count, 41 | ) 42 | if curr_p_adic_valuation > max_p_adic_valuation: 43 | max_p_adic_valuation = curr_p_adic_valuation 44 | critical_orient = [j] 45 | elif curr_p_adic_valuation == max_p_adic_valuation: 46 | critical_orient.append(j) 47 | if permutation_order == 1: 48 | if always_orient is None: 49 | always_orient = [j] 50 | else: 51 | always_orient.append(j) 52 | 53 | orient_count = 0 if always_orient is None else len(always_orient) 54 | critical_is_disjoint = critical_orient is not None and ( 55 | always_orient is None or all(j not in always_orient for j in critical_orient) 56 | ) 57 | if critical_is_disjoint: 58 | orient_count += 1 59 | unorient_critical = orient_count == len(partition) and ( 60 | orientation_count == 2 61 | and orient_count % 2 == 1 62 | or orientation_count > 2 63 | and orient_count == 1 64 | ) 65 | if unorient_critical: 66 | if critical_is_disjoint: 67 | return order 68 | else: 69 | return None 70 | else: 71 | if orient_count == 0: 72 | return order 73 | else: 74 | return order * orientation_count 75 | 76 | 77 | def full_integer_partitions(cycle_cubie_count, orientation_count): 78 | partitions = [ 79 | (order, partition) 80 | for partition in integer_partitions(cycle_cubie_count) 81 | if (order := partition_order(partition, orientation_count)) is not None 82 | ] 83 | partitions.sort(reverse=True, key=operator.itemgetter(0)) 84 | return partitions 85 | 86 | 87 | def reduced_integer_partitions(cycle_cubie_count, orientation_count, parity_aware): 88 | partitions = full_integer_partitions(cycle_cubie_count, orientation_count) 89 | 90 | dominated = [False] * len(partitions) 91 | reduced_partitions = [] 92 | for i in range(len(partitions)): 93 | if dominated[i]: 94 | continue 95 | partition = partitions[i] 96 | reduced_partitions.append(partition) 97 | for j in range(i + 1, len(partitions)): 98 | if ( 99 | partition[0] % partitions[j][0] == 0 100 | and partition[0] != partitions[j][0] 101 | and ( 102 | not parity_aware 103 | or ( 104 | sum(partition[1]) 105 | + len(partition[1]) 106 | + sum(partitions[j][1]) 107 | + len(partitions[j][1]) 108 | ) 109 | % 2 110 | == 0 111 | ) 112 | ): 113 | dominated[j] = True 114 | return reduced_partitions 115 | 116 | 117 | # list of (order, partition of N) 118 | # example: 119 | # corners_constraint == [(45, (3, 5)), (36, (1, 3, 4)), (30, (1, 2, 5)), (21, (1, 7)), (18, (1, 2, 2, 3)), (18, (2, 6)), (12, (1, 1, 2, 4)), (12, (4, 4)), (8, (8,))] 120 | 121 | edges_constraint = reduced_integer_partitions(12, 2, True) 122 | corners_constraint = reduced_integer_partitions(8, 3, True) 123 | s24_noconstraint = reduced_integer_partitions(24, 1, False) 124 | s24_constraint = reduced_integer_partitions(24, 1, True) 125 | # TODO: asher and I discussed needing the full integer partitions for larger 126 | # cubes. this is unfortunately very very slow 127 | # edges_constraint = full_integer_partitions(12, 2) 128 | # corners_constraint = full_integer_partitions(8, 3) 129 | # s24_noconstraint = full_integer_partitions(24, 1) 130 | # s24_constraint = full_integer_partitions(24, 1) 131 | 132 | # the first element of the tuple is the index where everything then and after 133 | # wards are identical orbits (s24). This is used to enforce a constraint that 134 | # the partitions must be in ascending order for these orbits to avoid duplicates 135 | 136 | 137 | _3x3 = ( 138 | 2, 139 | [ 140 | edges_constraint, 141 | corners_constraint, 142 | ], 143 | ) 144 | 145 | _4x4 = ( 146 | 1, 147 | [ 148 | corners_constraint, 149 | s24_noconstraint, 150 | s24_constraint, 151 | ], 152 | ) 153 | 154 | _5x5 = ( 155 | 2, 156 | [ 157 | edges_constraint, 158 | corners_constraint, 159 | s24_constraint, 160 | s24_constraint, 161 | s24_constraint, 162 | ], 163 | ) 164 | 165 | 166 | _6x6 = ( 167 | 1, 168 | [ 169 | corners_constraint, 170 | # these *should* be s24_constraint but it's really slow :( 171 | s24_noconstraint, 172 | s24_noconstraint, 173 | s24_noconstraint, 174 | s24_noconstraint, 175 | s24_noconstraint, 176 | s24_noconstraint, 177 | ], 178 | ) 179 | 180 | # GOAL: make 7x7 and onwards fast 181 | 182 | _7x7 = ( 183 | 2, 184 | [ 185 | edges_constraint, 186 | corners_constraint, 187 | s24_noconstraint, 188 | s24_noconstraint, 189 | s24_noconstraint, 190 | s24_noconstraint, 191 | s24_noconstraint, 192 | s24_noconstraint, 193 | s24_noconstraint, 194 | s24_noconstraint, 195 | ], 196 | ) 197 | 198 | 199 | # big_cube is unused 200 | def highest_order_partitions(puzzle, debug, big_cube): 201 | identical_index, all_reduced_integer_partitions = puzzle 202 | count = 0 203 | highest_order = -1 204 | rest_upper_bounds = [] 205 | cycles = [] 206 | rest_upper_bound = 1 207 | 208 | for lcm_and_partition in map( 209 | operator.itemgetter(0), all_reduced_integer_partitions 210 | ): 211 | rest_upper_bounds.append(rest_upper_bound) 212 | rest_upper_bound *= lcm_and_partition[0] 213 | 214 | heap = [] 215 | # NOTE: heapq is not efficient! there are more efficient priority queue 216 | # data structures that exist (strict fibonacci heaps) but we use heapq 217 | # for simplicity. 218 | heapq.heappush(heap, (1, len(all_reduced_integer_partitions) - 1, 1, [])) 219 | if debug: 220 | t = 0 221 | while heap: 222 | if debug: 223 | if t % 10000 == 0: 224 | print(f"The heap has {len(heap)} elements") 225 | t += 1 226 | _, i, running_order, cubie_partition_objs = heapq.heappop(heap) 227 | 228 | if i == -1: 229 | if running_order > highest_order: 230 | cycles.clear() 231 | if running_order < highest_order: 232 | continue 233 | highest_order = running_order 234 | cycles.append(cubie_partition_objs) 235 | if debug: 236 | print(f"New highest order: {highest_order}") 237 | print(f"Cycles: {cycles}") 238 | continue 239 | 240 | for lcm_and_partition in all_reduced_integer_partitions[i]: 241 | count += 1 242 | lcm, partition = lcm_and_partition 243 | rest_upper_bound = running_order * lcm 244 | if rest_upper_bound * rest_upper_bounds[i] < highest_order: 245 | break 246 | gcd = math.gcd(running_order, lcm) 247 | if ( 248 | # does the current index refer to an identical orbit (s24) 249 | i >= identical_index 250 | # if so, then enforce p1 < p2 < p3 ... < pn for all partitions 251 | # to ensure no duplicates are generated. It is assumed that the 252 | # caller will manually permute these identical partitions/ 253 | # TODO: how should duplicates be handled? 254 | and cubie_partition_objs 255 | and partition > cubie_partition_objs[0] 256 | ): 257 | continue 258 | heapq.heappush( 259 | heap, 260 | ( 261 | # adding `gcd` in front makes it faster, but not sure why 262 | gcd, 263 | # the order of the next two arguments is insignificant. I am 264 | # getting slight performance improvements with this order. 265 | i - 1, 266 | rest_upper_bound // gcd, 267 | # there are probably more efficient ways to create a new list 268 | # every iteration, but I will leave it like this for the sake 269 | # of not making the code more complicated than it already is 270 | [partition] + cubie_partition_objs, 271 | ), 272 | ) 273 | print(f"Took {count} loop iterations") 274 | return highest_order, cycles 275 | 276 | 277 | WRITE_TO_FILE = True 278 | 279 | start = default_timer() 280 | results = highest_order_partitions(_5x5, True, False) 281 | end = default_timer() - start 282 | print(f"Generated {len(results[1])} unique results in {end:.3g}s") 283 | if WRITE_TO_FILE: 284 | with open("output.py", "w") as f: 285 | f.write( 286 | f"# Highest order: {results[0]}\n# Run `python -i output.py`\n\nresults = {results[1]}" 287 | ) 288 | else: 289 | print(f"\nHighest order: {results[0]}\n{results[1]}") 290 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase1/common_types.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import dataclasses 3 | import enum 4 | 5 | 6 | class OrientationSumConstraint(enum.Enum): 7 | ZERO = enum.auto() 8 | NONE = enum.auto() 9 | 10 | 11 | class OrientationStatus: 12 | @dataclasses.dataclass(frozen=True) 13 | class CannotOrient: 14 | pass 15 | 16 | @dataclasses.dataclass(frozen=True, unsafe_hash=True) 17 | class CanOrient: 18 | count: int 19 | sum_constraint: OrientationSumConstraint 20 | 21 | 22 | PuzzleOrbitDefinition = collections.namedtuple( 23 | "PuzzleOrbitDefinition", 24 | [ 25 | "orbits", 26 | "even_parity_constraints", 27 | ], 28 | ) 29 | 30 | 31 | Orbit = collections.namedtuple( 32 | "Orbit", 33 | [ 34 | "name", 35 | "cubie_count", 36 | "orientation_status", 37 | ], 38 | ) 39 | 40 | EvenParityConstraint = collections.namedtuple( 41 | "EqualPermutationCombinations", 42 | [ 43 | "orbit_names", 44 | ], 45 | ) 46 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase1/puzzle_orbit_definitions.py: -------------------------------------------------------------------------------- 1 | from common_types import ( 2 | PuzzleOrbitDefinition, 3 | OrientationSumConstraint, 4 | OrientationStatus, 5 | Orbit, 6 | EvenParityConstraint, 7 | ) 8 | 9 | 10 | def cube(N): 11 | # start with corners since all sized cubes N>1 have 8 corners 12 | orbits = [ 13 | Orbit( 14 | name="corners", 15 | cubie_count=8, 16 | orientation_status=OrientationStatus.CanOrient( 17 | count=3, 18 | sum_constraint=OrientationSumConstraint.ZERO, 19 | ), 20 | ), 21 | ] 22 | even_parity_constraints = [] 23 | 24 | if N % 2 == 1: 25 | # if N is odd, the cube has 12 edges and the edge parity is equivalent to corner parity 26 | orbits.append( 27 | Orbit( 28 | name="edges", 29 | cubie_count=12, 30 | orientation_status=OrientationStatus.CanOrient( 31 | count=2, 32 | sum_constraint=OrientationSumConstraint.ZERO, 33 | ), 34 | ), 35 | ) 36 | even_parity_constraints.append( 37 | EvenParityConstraint( 38 | orbit_names=( 39 | "edges", 40 | "corners", 41 | ), 42 | ), 43 | ) 44 | 45 | # if N is odd, the cube has N//2 - 1 sets of 24 centers. these are called +centers since they form a + shape 46 | # each has parity determined by the corners and the wings it shares a slice with 47 | for c2 in range(1, N // 2): 48 | orbits.append( 49 | Orbit( 50 | name=f"+centers{c2}", 51 | cubie_count=24, 52 | orientation_status=OrientationStatus.CannotOrient(), 53 | ), 54 | ) 55 | even_parity_constraints.append( 56 | EvenParityConstraint( 57 | orbit_names=( 58 | "corners", 59 | f"wings{c2}", 60 | f"+centers{c2}", 61 | ), 62 | ), 63 | ) 64 | 65 | # the cube has N//2 - 1 sets of 24 wings. 66 | for w in range(1, N // 2): 67 | orbits.append( 68 | Orbit( 69 | name=f"wings{w}", 70 | cubie_count=24, 71 | orientation_status=OrientationStatus.CannotOrient(), 72 | ), 73 | ) 74 | 75 | # the cube has (N//2 - 1)^2 sets of 24 centers. 76 | for c1 in range(1, N // 2): 77 | for c2 in range(1, N // 2): 78 | # the centers with equal indices are called xcenters since they form an x shape 79 | # xcenter parity is only determined by the corners, since the associated wing parity doubles and therefore always cancels out 80 | if c1 == c2: 81 | orbits.append( 82 | Orbit( 83 | name=f"xcenters{c1}", 84 | cubie_count=24, 85 | orientation_status=OrientationStatus.CannotOrient(), 86 | ), 87 | ) 88 | even_parity_constraints.append( 89 | EvenParityConstraint( 90 | orbit_names=( 91 | "corners", 92 | f"xcenters{c1}", 93 | ), 94 | ), 95 | ) 96 | 97 | # the other centers are called obliques, they fall on a skewed slope from the cube's sides 98 | # oblique parity is determined by the corners, and both sets of wings that it shares a slice with 99 | else: 100 | orbits.append( 101 | Orbit( 102 | name=f"obliques{c1};{c2}", 103 | cubie_count=24, 104 | orientation_status=OrientationStatus.CannotOrient(), 105 | ), 106 | ) 107 | even_parity_constraints.append( 108 | EvenParityConstraint( 109 | orbit_names=( 110 | "corners", 111 | f"wings{c1}", 112 | f"wings{c2}", 113 | f"obliques{c1};{c2}", 114 | ), 115 | ), 116 | ) 117 | 118 | return PuzzleOrbitDefinition( 119 | orbits=tuple(orbits), 120 | even_parity_constraints=tuple(even_parity_constraints), 121 | ) 122 | 123 | 124 | def minx(N): 125 | # start with corners since all sized minxes N>1 have 20 corners 126 | orbits = [ 127 | Orbit( 128 | name="corners", 129 | cubie_count=20, 130 | orientation_status=OrientationStatus.CanOrient( 131 | count=3, 132 | sum_constraint=OrientationSumConstraint.ZERO, 133 | ), 134 | ), 135 | ] 136 | even_parity_constraints = [] 137 | 138 | # all piece types on the minxes must have even parity since every move induces only 5-cycles 139 | even_parity_constraints.append( 140 | EvenParityConstraint( 141 | orbit_names=("corners",), 142 | ), 143 | ) 144 | 145 | if N % 2 == 1: 146 | # if N is odd, the minx has 30 edges 147 | orbits.append( 148 | Orbit( 149 | name="edges", 150 | cubie_count=30, 151 | orientation_status=OrientationStatus.CanOrient( 152 | count=2, 153 | sum_constraint=OrientationSumConstraint.ZERO, 154 | ), 155 | ), 156 | ) 157 | even_parity_constraints.append( 158 | EvenParityConstraint( 159 | orbit_names=("edges",), 160 | ), 161 | ) 162 | 163 | # if N is odd, the minx has N//2 - 1 sets of 60 +centers 164 | for c2 in range(1, N // 2): 165 | orbits.append( 166 | Orbit( 167 | name=f"+centers{c2}", 168 | cubie_count=60, 169 | orientation_status=OrientationStatus.CannotOrient(), 170 | ), 171 | ) 172 | even_parity_constraints.append( 173 | EvenParityConstraint( 174 | orbit_names=(f"+centers{c2}",), 175 | ), 176 | ) 177 | 178 | # the minx has N//2 - 1 sets of 60 wings. 179 | for w in range(1, N // 2): 180 | orbits.append( 181 | Orbit( 182 | name=f"wings{w}", 183 | cubie_count=60, 184 | orientation_status=OrientationStatus.CannotOrient(), 185 | ), 186 | ) 187 | even_parity_constraints.append( 188 | EvenParityConstraint( 189 | orbit_names=(f"wings{w}",), 190 | ), 191 | ) 192 | 193 | # the minx has (N//2 - 1)^2 sets of 60 centers. 194 | for c1 in range(1, N // 2): 195 | for c2 in range(1, N // 2): 196 | # the centers with equal indices are called xcenters, following from the cube naming 197 | if c1 == c2: 198 | orbits.append( 199 | Orbit( 200 | name=f"xcenters{c1}", 201 | cubie_count=60, 202 | orientation_status=OrientationStatus.CannotOrient(), 203 | ), 204 | ) 205 | even_parity_constraints.append( 206 | EvenParityConstraint( 207 | orbit_names=(f"xcenters{c1}",), 208 | ), 209 | ) 210 | 211 | # the other centers are called obliques 212 | else: 213 | orbits.append( 214 | Orbit( 215 | name=f"obliques{c1};{c2}", 216 | cubie_count=60, 217 | orientation_status=OrientationStatus.CannotOrient(), 218 | ), 219 | ) 220 | even_parity_constraints.append( 221 | EvenParityConstraint( 222 | orbit_names=(f"obliques{c1};{c2}",), 223 | ), 224 | ) 225 | 226 | return PuzzleOrbitDefinition( 227 | orbits=tuple(orbits), 228 | even_parity_constraints=tuple(even_parity_constraints), 229 | ) 230 | 231 | 232 | PUZZLE_2x2 = cube(2) 233 | PUZZLE_3x3 = cube(3) 234 | PUZZLE_4x4 = cube(4) 235 | PUZZLE_5x5 = cube(5) 236 | PUZZLE_6x6 = cube(6) 237 | PUZZLE_KILOMINX = minx(2) 238 | PUZZLE_MEGAMINX = minx(3) 239 | PUZZLE_MASTERKILOMINX = minx(4) 240 | PUZZLE_GIGAMINX = minx(5) 241 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2-GAP-experimental/phase2.g: -------------------------------------------------------------------------------- 1 | LoadPackage("datastructures"); 2 | 3 | U := ( 1, 3, 8, 6)( 2, 5, 7, 4)( 9,33,25,17)(10,34,26,18)(11,35,27,19); 4 | L := ( 9,11,16,14)(10,13,15,12)( 1,17,41,40)( 4,20,44,37)( 6,22,46,35); 5 | F := (17,19,24,22)(18,21,23,20)( 6,25,43,16)( 7,28,42,13)( 8,30,41,11); 6 | R := (25,27,32,30)(26,29,31,28)( 3,38,43,19)( 5,36,45,21)( 8,33,48,24); 7 | B := (33,35,40,38)(34,37,39,36)( 3, 9,46,32)( 2,12,47,29)( 1,14,48,27); 8 | D := (41,43,48,46)(42,45,47,44)(14,22,30,38)(15,23,31,39)(16,24,32,40); 9 | cube := Group(U, L, F, R, B, D); 10 | edge_facelet_buf := 42; 11 | corner_facelet_buf := 1; 12 | edge_cubies := Blocks(cube, Orbit(cube, edge_facelet_buf)); 13 | corner_cubies := Blocks(cube, Orbit(cube, corner_facelet_buf)); 14 | edge_facelets := Immutable(Flat(edge_cubies)); 15 | corner_facelets := Immutable(Flat(corner_cubies)); 16 | 17 | Read("./util.g"); 18 | 19 | cornercube := ClosureGroup( 20 | Stabilizer(cube, Orbit(cube, edge_facelet_buf), OnTuples), 21 | (3, 8)(19, 27)(25, 33) 22 | ); 23 | cornercube_enumerator := Enumerator(cornercube); 24 | moves := [U, L, F, R, B, D, U^-1, L^-1, F^-1, R^-1, B^-1, D^-1, U^2, L^2, F^2, R^2, B^2, D^2];; 25 | 26 | len_moves := Length(moves); 27 | target_cycle_structure_corners := [ ,2 ]; 28 | target_cycle_structure := [ 1,2,,,,,,,1 ]; 29 | # phase 2 currently has a logic error, it finds the first cycle with the cycle type but not every cycle same htm/qtm which are all candidates for phase 3 30 | HashPerm := function(perm) 31 | # TODO: use lehmer code + ternary instead 32 | return Position(cornercube_enumerator, perm); 33 | end; 34 | 35 | BFSFromStructure := function(target_cycle_structure_corners) 36 | local queue, perm, curr_depth, new_perm, j, i, distances, class, 37 | classes, queue_length, k, hash_perm, cache, visited_count, start; 38 | start := Runtime(); 39 | Print("** Finding conjugacy classes **\n"); 40 | classes := ConjugacyClassesOfStructure(cornercube, target_cycle_structure_corners); 41 | Print("** Initializing search **\n"); 42 | distances := ListWithIdenticalEntries(Size(cornercube), -1); 43 | queue := PlistDeque(); 44 | visited_count := 0; 45 | for class in classes do 46 | for perm in class do 47 | distances[HashPerm(perm)] := 0; 48 | PlistDequePushBack(queue, perm); 49 | od; 50 | visited_count := visited_count + Size(class); 51 | od; 52 | Unbind(class); 53 | Unbind(classes); 54 | Print("** Running BFS **\n"); 55 | curr_depth := 0; 56 | while not IsEmpty(queue) do 57 | queue_length := Size(queue); 58 | curr_depth := curr_depth + 1; 59 | Print("** Populating BFS depth ", curr_depth, " exploring ", queue_length, " nodes (", Int(visited_count / Size(cornercube) * 100), "%) done **\n"); 60 | for i in [1..queue_length] do 61 | perm := PlistDequePopFront(queue); 62 | for j in [1..18] do 63 | new_perm := ListPerm(perm * moves[j]); 64 | for k in edge_facelets do 65 | if k <= Length(new_perm) then 66 | new_perm[k] := k; 67 | fi; 68 | od; 69 | new_perm := PermList(new_perm); 70 | hash_perm := HashPerm(new_perm); 71 | if distances[hash_perm] = -1 or curr_depth < distances[hash_perm] then 72 | if distances[hash_perm] = -1 then 73 | visited_count := visited_count + 1; 74 | fi; 75 | distances[hash_perm] := curr_depth; 76 | if visited_count = Size(cornercube) then 77 | Print("** Generated pruning table in ", Runtime() - start, "ms **\n"); 78 | return distances; 79 | fi; 80 | PlistDequePushBack(queue, new_perm); 81 | fi; 82 | od; 83 | od; 84 | od; 85 | Print("** Generated pruning table in ", Runtime() - start, "ms **\n"); 86 | return distances; 87 | end; 88 | 89 | heuristic := BFSFromStructure(target_cycle_structure_corners);; 90 | SaveWorkspace("save11"); 91 | 92 | IDAStarSearch := function(path, last_state, g, bound) 93 | local last_h, f, min, i, last_move_index_mod, next_state, j, t; 94 | next_state := ListPerm(last_state); 95 | for j in edge_facelets do 96 | if j <= Length(next_state) then 97 | next_state[j] := j; 98 | fi; 99 | od; 100 | next_state := PermList(next_state); 101 | last_h := heuristic[HashPerm(next_state)]; 102 | f := g + last_h; 103 | if f > bound then 104 | return f; 105 | elif CycleStructurePerm(last_state) = target_cycle_structure then 106 | return -1; 107 | else 108 | min := 999999; 109 | if Length(path) > 0 then 110 | last_move_index_mod := path[Length(path)] mod 6; 111 | fi; 112 | for i in [1..len_moves] do 113 | if Length(path) > 0 and ((last_move_index_mod = 2 and i mod 6 = 4) or 114 | (last_move_index_mod = 1 and i mod 6 = 0) or 115 | (last_move_index_mod = 3 and i mod 6 = 5) or 116 | (last_move_index_mod = i mod 6)) 117 | then 118 | continue; 119 | fi; 120 | Add(path, i); 121 | t := IDAStarSearch(path, last_state * moves[i], g + 1, bound); 122 | if t = -1 then 123 | return t; 124 | elif t < min then 125 | min := t; 126 | fi; 127 | Remove(path); 128 | od; 129 | return min; 130 | fi; 131 | end; 132 | 133 | IDAStar := function() 134 | local start_state, bound, start, path; 135 | start_state := (); 136 | bound := heuristic[HashPerm(start_state)]; 137 | start := Runtime(); 138 | path := []; 139 | Print("** Starting IDA star **\n"); 140 | while true do 141 | Print("** Searching depth ", bound, " **\n"); 142 | bound := IDAStarSearch(path, start_state, 0, bound); 143 | if bound = -1 then 144 | Print("** Found solution in ", Runtime() - start, "ms **\n"); 145 | return path; 146 | else 147 | Print("** Increasing bound to ", bound, " **\n"); 148 | fi; 149 | od; 150 | end; 151 | 152 | a:=IDAStar(); 153 | Print(a); 154 | AppendTo("./awesome.txt", a); -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2-GAP-experimental/phase3.g: -------------------------------------------------------------------------------- 1 | prev_phase_info := function() 2 | return rec( 3 | first_cycles := Immutable([ 4 | U*L*B^-1*L*B^-1*U*R^-1*D*U^2*L^2*F^2, 5 | ]), 6 | second_cycle_order := 18, 7 | share_edge := true, 8 | share_corner := true, 9 | ); 10 | end; 11 | 12 | SetPrintFormattingStatus("*stdout*", false); 13 | U := ( 1, 3, 8, 6)( 2, 5, 7, 4)( 9,33,25,17)(10,34,26,18)(11,35,27,19); 14 | L := ( 9,11,16,14)(10,13,15,12)( 1,17,41,40)( 4,20,44,37)( 6,22,46,35); 15 | F := (17,19,24,22)(18,21,23,20)( 6,25,43,16)( 7,28,42,13)( 8,30,41,11); 16 | R := (25,27,32,30)(26,29,31,28)( 3,38,43,19)( 5,36,45,21)( 8,33,48,24); 17 | B := (33,35,40,38)(34,37,39,36)( 3, 9,46,32)( 2,12,47,29)( 1,14,48,27); 18 | D := (41,43,48,46)(42,45,47,44)(14,22,30,38)(15,23,31,39)(16,24,32,40); 19 | cube := Group(U, L, F, R, B, D); 20 | prev_phase_info := prev_phase_info(); 21 | edge_facelet_buf := 42; 22 | corner_facelet_buf := 1; 23 | edge_cubies := Blocks(cube, Orbit(cube, edge_facelet_buf)); 24 | corner_cubies := Blocks(cube, Orbit(cube, corner_facelet_buf)); 25 | edge_facelets := Immutable(Flat(edge_cubies)); 26 | corner_facelets := Immutable(Flat(corner_cubies)); 27 | 28 | Read("util.g"); 29 | 30 | ValidCornerFlip := function(corner1, corner2) 31 | if corner1 * corner2 in cube then 32 | return corner1 * corner2; 33 | else 34 | return corner1 * corner2^-1; 35 | fi; 36 | end; 37 | 38 | # TODO: try longer phase 1 cycles from phase 2, they might result in a shorter 39 | # phase 3 cycle 40 | 41 | # ALL WAYS TO ARRANGE THE REST OF N - 1 cycles 42 | # ADDENDUM: no because we care about the shortest algorithm for the most order 43 | # cycle, symmetry when all cycle orders are the same is handled in phase 1 44 | # ADDENDUM2: DONT DO THIS IN PHASE 1 DO IT HERE 45 | 46 | for i in [1..Length(prev_phase_info.first_cycles)] do 47 | # first_cycle_moved_points:=Unique(Concatenation(first_cycle_moved_points, MovedPoints(tmp))); 48 | first_cycle := prev_phase_info.first_cycles[i]; 49 | # Given our second cycle algorithm, we want to find the third 50 | first_cycle_moved_points := MovedPoints(first_cycle); 51 | first_cycle_stabilizer := Stabilizer( 52 | cube, 53 | first_cycle_moved_points, 54 | OnTuples 55 | ); 56 | first_cycle_components := Cycles(first_cycle, first_cycle_moved_points); 57 | first_cycle_stabilizer_edge_cubies := Blocks( 58 | first_cycle_stabilizer, 59 | Difference(edge_facelets, first_cycle_moved_points) 60 | ); 61 | first_cycle_stabilizer_corner_cubies := Blocks( 62 | first_cycle_stabilizer, 63 | Difference(corner_facelets, first_cycle_moved_points) 64 | ); 65 | 66 | # List(first_cycle_stabilizer_corner_cubies, c -> ValidCornerFlip(CycleFromList(c), shared_corner_cubie)); 67 | 68 | if prev_phase_info.share_edge or prev_phase_info.share_corner then 69 | shared_cubies := []; 70 | if prev_phase_info.share_edge then 71 | shared_edge_cubie := CycleFromList(First( 72 | first_cycle_components, 73 | c -> Length(c) = 2 74 | )); 75 | for second_edge_cubie in first_cycle_stabilizer_edge_cubies do 76 | Add( 77 | shared_cubies, 78 | CycleFromList(second_edge_cubie) * shared_edge_cubie 79 | ); 80 | od; 81 | fi; 82 | if prev_phase_info.share_corner then 83 | # TODO: multiple shared corner cubies, use ALL previous unshared cubies 84 | shared_corner_cubie := CycleFromList(First( 85 | first_cycle_components, 86 | c -> Length(c) = 3 87 | )); 88 | for second_corner_cubie in first_cycle_stabilizer_corner_cubies do 89 | Add( 90 | shared_cubies, 91 | ValidCornerFlip( 92 | CycleFromList(second_corner_cubie), 93 | shared_corner_cubie 94 | ) 95 | ); 96 | od; 97 | fi; 98 | second_cycle_group := ClosureGroup( 99 | first_cycle_stabilizer, 100 | shared_cubies 101 | ); 102 | else 103 | second_cycle_group := first_cycle_stabilizer; 104 | fi; 105 | 106 | second_cycle_classes := ConjugacyClassesOfOrder( 107 | second_cycle_group, 108 | prev_phase_info.second_cycle_order 109 | ); 110 | # second_cycle_classes := ConjugacyClassesOfStructure(second_cycle_group, [ 1,,,,,,,,1 ]); 111 | # TODO: FORCE FLIP SHARED CUBIE, FILTER CLASSES 112 | unique_length := 0; 113 | for j in [1..Length(second_cycle_classes)] do 114 | is_duplicate := false; 115 | inversed_class_element := Representative(second_cycle_classes[j])^-1; 116 | for k in [1..unique_length] do 117 | if inversed_class_element in second_cycle_classes[k] then 118 | is_duplicate := true; 119 | break; 120 | fi; 121 | od; 122 | if is_duplicate then 123 | continue; 124 | fi; 125 | unique_length := unique_length + 1; 126 | second_cycle_classes[unique_length] := second_cycle_classes[j]; 127 | prune_inverse_elements := ( 128 | inversed_class_element <> inversed_class_element^-1 129 | and inversed_class_element in second_cycle_classes[j] 130 | ); 131 | for second_cycle_candidate in second_cycle_classes[j] do 132 | # thing := Cycles(second_cycle_candidate, MovedPoints(second_cycle_candidate)); 133 | if ( 134 | not prune_inverse_elements or 135 | second_cycle_candidate < second_cycle_candidate^-1 136 | # ) and [7, 18] in thing and ([1, 35, 9] in thing or [1, 9, 35] in thing) then 137 | ) then 138 | AppendTo( 139 | "./output2.txt", 140 | PermutationSpeffz(second_cycle_candidate), 141 | "\n" 142 | ); 143 | fi; 144 | od; 145 | od; 146 | # TODO: for every thing the same htm and qtm size. have a stack of shared cubies each cycle combination uses 147 | od; 148 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2-GAP-experimental/reader.g: -------------------------------------------------------------------------------- 1 | # does not work; experimental 2 | 3 | stream := InputOutputLocalProcess(DirectoryCurrent(), "../../vcube/vc-optimal", [ "--coord=404", "--format=speffz" ]); 4 | 5 | U := ( 1, 3, 8, 6)( 2, 5, 7, 4)( 9,33,25,17)(10,34,26,18)(11,35,27,19); 6 | L := ( 9,11,16,14)(10,13,15,12)( 1,17,41,40)( 4,20,44,37)( 6,22,46,35); 7 | F := (17,19,24,22)(18,21,23,20)( 6,25,43,16)( 7,28,42,13)( 8,30,41,11); 8 | R := (25,27,32,30)(26,29,31,28)( 3,38,43,19)( 5,36,45,21)( 8,33,48,24); 9 | B := (33,35,40,38)(34,37,39,36)( 3, 9,46,32)( 2,12,47,29)( 1,14,48,27); 10 | D := (41,43,48,46)(42,45,47,44)(14,22,30,38)(15,23,31,39)(16,24,32,40); 11 | cube := Group(U, L, F, R, B, D); 12 | edge_facelet_buf := 42; 13 | corner_facelet_buf := 1; 14 | edge_cubies := Blocks(cube, Orbit(cube, edge_facelet_buf)); 15 | corner_cubies := Blocks(cube, Orbit(cube, corner_facelet_buf)); 16 | edge_facelets := Immutable(Flat(edge_cubies));g 17 | corner_facelets := Immutable(Flat(corner_cubies)); 18 | 19 | Read("phase3/util.g"); 20 | 21 | outFile := OutputTextFile("out.txt", false); 22 | for i in ConjugacyClass(cube, (1,3,6,9,33,17,35,27,11)(2,13,44,4,45,34,20,15,10,31)(7,18)(14,40,46)) do 23 | j := PermutationSpeffz(i); 24 | Add(j, '\n'); 25 | WriteLine(stream, j); 26 | line := ReadAllLine(stream); 27 | if line <> fail then 28 | WriteAll(outFile, line); 29 | fi; 30 | od; 31 | 32 | CloseStream(stream); 33 | CloseStream(outFile); -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2-GAP-experimental/testutil.g: -------------------------------------------------------------------------------- 1 | U := ( 1, 3, 8, 6)( 2, 5, 7, 4)( 9,33,25,17)(10,34,26,18)(11,35,27,19); 2 | L := ( 9,11,16,14)(10,13,15,12)( 1,17,41,40)( 4,20,44,37)( 6,22,46,35); 3 | F := (17,19,24,22)(18,21,23,20)( 6,25,43,16)( 7,28,42,13)( 8,30,41,11); 4 | R := (25,27,32,30)(26,29,31,28)( 3,38,43,19)( 5,36,45,21)( 8,33,48,24); 5 | B := (33,35,40,38)(34,37,39,36)( 3, 9,46,32)( 2,12,47,29)( 1,14,48,27); 6 | D := (41,43,48,46)(42,45,47,44)(14,22,30,38)(15,23,31,39)(16,24,32,40); 7 | cube := Group(U, L, F, R, B, D); 8 | edge_facelet_buf := 42; 9 | corner_facelet_buf := 1; 10 | edge_cubies := Blocks(cube, Orbit(cube, edge_facelet_buf)); 11 | corner_cubies := Blocks(cube, Orbit(cube, corner_facelet_buf)); 12 | edge_facelets := Immutable(Flat(edge_cubies)); 13 | corner_facelets := Immutable(Flat(corner_cubies)); 14 | 15 | Read("util.g"); 16 | moves := [U, L, F, R, B, D, U^-1, L^-1, F^-1, R^-1, B^-1, D^-1, U^2, L^2, F^2, R^2, B^2, D^2];; 17 | for i in [1..10000] do 18 | s := []; 19 | for j in [1..15] do 20 | Add(s, RandomList([1..48])); 21 | od; 22 | s := Stabilizer(cube, s, OnTuples); 23 | if Size(s) > 10000000 then continue; fi; 24 | for j in Set(List(ConjugacyClasses(s), x -> CycleStructurePerm(Representative(x)))) do 25 | a := ConjugacyClassesOfStructure(s, j); 26 | b := Filtered(ConjugacyClasses(s), x -> CycleStructurePerm(Representative(x)) = j); 27 | if Length(a) <> Length(b) then 28 | Error("Test failed"); 29 | fi; 30 | for j in a do 31 | if not j in b then 32 | Error("Test failed"); 33 | fi; 34 | od; 35 | for j in b do 36 | if not j in a then 37 | Error("Test failed"); 38 | fi; 39 | od; 40 | od; 41 | Print("Test ", i, " passed\n"); 42 | od; 43 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod canonical_fsm; 2 | pub mod orbit_puzzle; 3 | pub mod permutator; 4 | pub mod pruning; 5 | pub mod puzzle; 6 | pub mod puzzle_state_history; 7 | pub mod solver; 8 | 9 | // We can do one more however it will overflow when adding more to it which is 10 | // common in context 11 | const FACT_UNTIL_19: [u64; 20] = { 12 | let mut arr = [0; 20]; 13 | arr[0] = 1; 14 | let mut i = 1; 15 | while i < arr.len() { 16 | arr[i] = arr[i - 1] * i as u64; 17 | i += 1; 18 | } 19 | arr 20 | }; 21 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/orbit_puzzle.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | FACT_UNTIL_19, 3 | puzzle::{MultiBvInterface, OrbitDef}, 4 | }; 5 | use std::{ 6 | hash::Hash, 7 | num::NonZeroU8, 8 | simd::{LaneCount, Simd, SupportedLaneCount, cmp::SimdPartialOrd, num::SimdUint}, 9 | }; 10 | 11 | pub mod cube3; 12 | pub mod slice_orbit_puzzle; 13 | 14 | pub trait OrbitPuzzleState { 15 | type MultiBv: MultiBvInterface; 16 | 17 | unsafe fn replace_compose(&mut self, a: &Self, b: &Self, orbit_def: OrbitDef); 18 | fn induces_sorted_cycle_type( 19 | &self, 20 | sorted_cycle_type_orbit: &[(NonZeroU8, bool)], 21 | orbit_def: OrbitDef, 22 | multi_bv: ::ReusableRef<'_>, 23 | ) -> bool; 24 | fn approximate_hash(&self) -> impl Hash; 25 | fn exact_hasher(&self, orbit_def: OrbitDef) -> u64; 26 | } 27 | 28 | pub trait OrbitPuzzleConstructors { 29 | type MultiBv: MultiBvInterface; 30 | 31 | fn new_multi_bv(orbit_def: OrbitDef) -> Self::MultiBv; 32 | fn from_orbit_transformation_unchecked>( 33 | perm: B, 34 | ori: B, 35 | orbit_def: OrbitDef, 36 | ) -> Self; 37 | } 38 | 39 | /// Efficently exactly hash an orbit into a u64, panicking at compile-time if 40 | /// not possible. This function uses a combination of SIMD lehmer coding and an 41 | /// efficient n-ary base hash. Uses `u16`s for const generics because usize 42 | /// implements From. 43 | pub(crate) fn exact_hasher_orbit( 44 | perm: Simd, 45 | ori: Simd, 46 | ) -> u64 47 | where 48 | LaneCount: SupportedLaneCount, 49 | { 50 | // Powers of ORI_COUNT used to efficiently hash the orientation to an n-ary 51 | // base. The hash is essentially a dot product of the orientation vector 52 | // with the powers of ORI_COUNT 53 | let powers: Simd = const { 54 | // Everything not a power must be zero to make sure nothing interferes 55 | let mut arr = [0; LEN]; 56 | let mut i = 0; 57 | // We do an important check that the next power does not overflow `u16`. 58 | // The dot product will eventually be collapsed to a value larger than 59 | // ORI_COUNT.pow(PIECE_COUNT - 2) but less than 60 | // ORI_COUNT.pow(PIECE_COUNT - 1). 61 | u16::checked_pow(ORI_COUNT, PIECE_COUNT as u32 - 1).unwrap(); 62 | // The sum of the orientation vector must be divisible by ORI_COUNT. 63 | // As a consequence, you don't need the last element to uniquely 64 | // identify an orientation vector, so we skip processing for it by 65 | // only computing powers up to PIECE_COUNT - 1 66 | while i < PIECE_COUNT - 1 { 67 | // Under the hood LLVM splits up the dot product calculation into 68 | // chunks of 128 bit registers so having a the smallest possible 69 | // data type (u16) is important 70 | arr[i as usize] = u16::checked_pow( 71 | ORI_COUNT, 72 | ( 73 | // The powers are computed in reverse order to match the 74 | // order of lexicographic permutation with replacement. 75 | // Reverse order in general is len - i - 1, and len is 76 | // PIECE_COUNT - 1 77 | (PIECE_COUNT - 1) - i - 1 78 | ) as u32, 79 | ) 80 | .unwrap(); 81 | i += 1; 82 | } 83 | Simd::::from_array(arr) 84 | }; 85 | // We compute: lehmer code * number_of_states(n-ary hash) + n-ary hash 86 | // 87 | // One thing to note about the last element for Lehmer codes is no matter 88 | // what, there will always be an equal number of elements to its left that 89 | // are less than it. This allows us to hard code it to 0 and iterate from 0 90 | // to PIECE_COUNT - 1 91 | (0..usize::from(PIECE_COUNT) - 1) 92 | .map(|i| { 93 | let lt_before_current_count = if i == 0 { 94 | // There are no elements left of the first element less than it 95 | u64::from(perm[0]) 96 | } else { 97 | // Count how many elements to the left of the current element 98 | // are less than it 99 | let lt_current_mask = perm.simd_lt(Simd::::splat(perm[i])); 100 | u64::from((lt_current_mask.to_bitmask() >> i).count_ones()) 101 | }; 102 | // FACT_UNTIL_19[i] = i! 103 | let fact = FACT_UNTIL_19[usize::from(PIECE_COUNT) - 1 - i]; 104 | lt_before_current_count * fact 105 | }) 106 | .sum::() 107 | // Orientation is a permutation with replacement. The number of states 108 | // is trivially ORI_COUNT.pow(PIECE_COUNT), but subtract one because the 109 | // last element is ignored as described above 110 | * u64::from(ORI_COUNT.pow(u32::from(PIECE_COUNT) - 1)) 111 | // Compute the aforementioned dot product 112 | + u64::from((ori.cast::() * powers).reduce_sum()) 113 | } 114 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/orbit_puzzle/cube3/avx2.rs: -------------------------------------------------------------------------------- 1 | //! An AVX2 optimized implementation for 3x3 orbits during pruning table 2 | //! generation. 3 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/orbit_puzzle/cube3/mod.rs: -------------------------------------------------------------------------------- 1 | //! SIMD optimized implementations for 3x3 orbits during pruning table 2 | //! generation. 3 | 4 | pub mod avx2; 5 | pub mod simd8and16; 6 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/orbit_puzzle/cube3/simd8and16.rs: -------------------------------------------------------------------------------- 1 | //! A SIMD optimized implementation for 3x3 orbits during pruning table 2 | //! generation for platforms that support 8 and 16 byte SIMD 3 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/orbit_puzzle/slice_orbit_puzzle.rs: -------------------------------------------------------------------------------- 1 | //! The default and fallback implementation for 3x3 orbits during pruning table 2 | //! generation. 3 | 4 | use super::{OrbitPuzzleConstructors, OrbitPuzzleState}; 5 | use crate::phase2::puzzle::{MultiBvInterface, OrbitDef, slice_puzzle::exact_hasher_orbit_bytes}; 6 | use std::{cmp::Ordering, num::NonZeroU8}; 7 | 8 | #[derive(Clone, PartialEq, Debug, Hash)] 9 | pub struct SliceOrbitPuzzle(Box<[u8]>); 10 | 11 | pub struct SliceOrbitMultiBv(Box<[u8]>); 12 | pub struct SliceOrbitMultiBvRefMut<'a>(&'a mut [u8]); 13 | 14 | impl MultiBvInterface for SliceOrbitMultiBv { 15 | type ReusableRef<'a> = SliceOrbitMultiBvRefMut<'a>; 16 | 17 | fn reusable_ref(&mut self) -> Self::ReusableRef<'_> { 18 | SliceOrbitMultiBvRefMut(&mut self.0) 19 | } 20 | } 21 | 22 | impl OrbitPuzzleState for SliceOrbitPuzzle { 23 | type MultiBv = SliceOrbitMultiBv; 24 | 25 | unsafe fn replace_compose(&mut self, a: &Self, b: &Self, orbit_def: OrbitDef) { 26 | unsafe { replace_compose_slice_orbit(&mut self.0, 0, &a.0, &b.0, orbit_def) }; 27 | } 28 | 29 | fn induces_sorted_cycle_type( 30 | &self, 31 | sorted_cycle_type_orbit: &[(NonZeroU8, bool)], 32 | orbit_def: OrbitDef, 33 | multi_bv: SliceOrbitMultiBvRefMut<'_>, 34 | ) -> bool { 35 | unsafe { 36 | induces_sorted_cycle_type_slice_orbit( 37 | &self.0, 38 | 0, 39 | sorted_cycle_type_orbit, 40 | orbit_def, 41 | multi_bv.0, 42 | ) 43 | } 44 | } 45 | 46 | #[allow(refining_impl_trait)] 47 | fn approximate_hash(&self) -> &Self { 48 | self 49 | } 50 | 51 | fn exact_hasher(&self, orbit_def: OrbitDef) -> u64 { 52 | let (perm, ori) = self.0.split_at(orbit_def.piece_count.get() as usize); 53 | exact_hasher_orbit_bytes(perm, ori, orbit_def) 54 | } 55 | } 56 | 57 | impl OrbitPuzzleConstructors for SliceOrbitPuzzle { 58 | type MultiBv = SliceOrbitMultiBv; 59 | 60 | fn new_multi_bv(orbit_def: OrbitDef) -> SliceOrbitMultiBv { 61 | SliceOrbitMultiBv( 62 | vec![0; orbit_def.piece_count.get().div_ceil(4) as usize].into_boxed_slice(), 63 | ) 64 | } 65 | 66 | fn from_orbit_transformation_unchecked>( 67 | perm: B, 68 | ori: B, 69 | orbit_def: OrbitDef, 70 | ) -> Self { 71 | let mut orbit_states = vec![0_u8; orbit_def.piece_count.get() as usize * 2]; 72 | let piece_count = orbit_def.piece_count.get(); 73 | for i in 0..piece_count { 74 | orbit_states[(piece_count + i) as usize] = ori.as_ref()[i as usize]; 75 | orbit_states[i as usize] = perm.as_ref()[i as usize]; 76 | } 77 | SliceOrbitPuzzle(orbit_states.into_boxed_slice()) 78 | } 79 | } 80 | 81 | pub unsafe fn replace_compose_slice_orbit( 82 | orbit_states_mut: &mut [u8], 83 | base: usize, 84 | a: &[u8], 85 | b: &[u8], 86 | orbit_def: OrbitDef, 87 | ) { 88 | let piece_count = orbit_def.piece_count.get() as usize; 89 | // SAFETY: Permutation vectors and orientation vectors are shuffled 90 | // around, based on code from twsearch [1]. Testing has shown this is 91 | // sound. 92 | // [1] https://github.com/cubing/twsearch 93 | if orbit_def.orientation_count == 1.try_into().unwrap() { 94 | for i in 0..piece_count { 95 | let base_i = base + i; 96 | unsafe { 97 | let pos = *a.get_unchecked(base + *b.get_unchecked(base_i) as usize); 98 | *orbit_states_mut.get_unchecked_mut(base_i) = pos; 99 | *orbit_states_mut.get_unchecked_mut(base_i + piece_count) = 0; 100 | } 101 | } 102 | } else { 103 | for i in 0..piece_count { 104 | let base_i = base + i; 105 | unsafe { 106 | let pos = a.get_unchecked(base + *b.get_unchecked(base_i) as usize); 107 | let a_ori = a.get_unchecked(base + *b.get_unchecked(base_i) as usize + piece_count); 108 | let b_ori = b.get_unchecked(base_i + piece_count); 109 | *orbit_states_mut.get_unchecked_mut(base_i) = *pos; 110 | *orbit_states_mut.get_unchecked_mut(base_i + piece_count) = (*a_ori + *b_ori) 111 | .min((*a_ori + *b_ori).wrapping_sub(orbit_def.orientation_count.get())); 112 | } 113 | } 114 | } 115 | } 116 | 117 | #[inline] 118 | pub unsafe fn induces_sorted_cycle_type_slice_orbit( 119 | orbit_states: &[u8], 120 | base: usize, 121 | sorted_cycle_type_orbit: &[(NonZeroU8, bool)], 122 | orbit_def: OrbitDef, 123 | multi_bv: &mut [u8], 124 | ) -> bool { 125 | multi_bv.fill(0); 126 | let mut covered_cycles_count = 0; 127 | let piece_count = orbit_def.piece_count.get() as usize; 128 | for i in 0..piece_count { 129 | let (div, rem) = (i / 4, i % 4); 130 | // SAFETY: default_multi_bv_slice ensures that (i / 4) always fits 131 | // in multi_bv 132 | if unsafe { *multi_bv.get_unchecked(div) } & (1 << rem) != 0 { 133 | continue; 134 | } 135 | // SAFETY: see above 136 | unsafe { 137 | *multi_bv.get_unchecked_mut(div) |= 1 << rem; 138 | } 139 | let mut actual_cycle_length = 1; 140 | // SAFETY: sorted_orbit_defs guarantees that base (the orbit state 141 | // base pointer) + i (a permutation vector element) is valid 142 | let mut piece = unsafe { *orbit_states.get_unchecked(base + i) } as usize; 143 | // SAFETY: sorted_orbit_defs guarantees that base (the orbit state 144 | // base pointer) + i + piece (an orientation vector element) is valid 145 | let mut orientation_sum = 146 | unsafe { *orbit_states.get_unchecked(base + piece + piece_count) }; 147 | 148 | while piece != i { 149 | actual_cycle_length += 1; 150 | let (div, rem) = (piece / 4, piece % 4); 151 | // SAFETY: default_multi_bv_slice ensures that (piece / 4) 152 | // always in fits in multi_bv 153 | unsafe { 154 | *multi_bv.get_unchecked_mut(div) |= 1 << rem; 155 | } 156 | // SAFETY: sorted_orbit_defs guarantees that base (the orbit 157 | // state base pointer) + piece (a permutation vector element) is 158 | // valid 159 | unsafe { 160 | piece = *orbit_states.get_unchecked(base + piece) as usize; 161 | } 162 | // SAFETY: sorted_orbit_defs guarantees that base (the orbit 163 | // state base pointer) + piece + piece_count (an orientation 164 | // vector element) is valid 165 | unsafe { 166 | orientation_sum += *orbit_states.get_unchecked(base + piece + piece_count); 167 | } 168 | } 169 | 170 | let actual_orients = orientation_sum % orbit_def.orientation_count != 0; 171 | if actual_cycle_length == 1 && !actual_orients { 172 | continue; 173 | } 174 | let mut valid_cycle_index = None; 175 | for (j, &(expected_cycle_length, expected_orients)) in 176 | sorted_cycle_type_orbit.iter().enumerate() 177 | { 178 | match expected_cycle_length.get().cmp(&actual_cycle_length) { 179 | Ordering::Less => (), 180 | Ordering::Equal => { 181 | let (div, rem) = (j / 4, j % 4); 182 | if expected_orients == actual_orients 183 | // SAFETY: default_multi_bv_slice ensures that (j / 4) 184 | // always fits in multi_bv 185 | && unsafe { *multi_bv.get_unchecked(div) } & (1 << (rem + 4)) == 0 186 | { 187 | valid_cycle_index = Some(j); 188 | break; 189 | } 190 | } 191 | Ordering::Greater => return false, 192 | } 193 | } 194 | let Some(valid_cycle_index) = valid_cycle_index else { 195 | return false; 196 | }; 197 | let (div, rem) = (valid_cycle_index / 4, valid_cycle_index % 4); 198 | // SAFETY: default_multi_bv_slice ensures that 199 | // (valid_cycle_index / 4) always fits in multi_bv 200 | unsafe { 201 | *multi_bv.get_unchecked_mut(div) |= 1 << (rem + 4); 202 | } 203 | covered_cycles_count += 1; 204 | // cannot possibly return true if this runs 205 | if covered_cycles_count > sorted_cycle_type_orbit.len() { 206 | return false; 207 | } 208 | } 209 | covered_cycles_count == sorted_cycle_type_orbit.len() 210 | } 211 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/pruning_simulation.py: -------------------------------------------------------------------------------- 1 | import random 2 | import math 3 | 4 | 5 | CORNERS_DIST = [ 6 | 1, 7 | 18, 8 | 243, 9 | 2874, 10 | 28000, 11 | 205416, 12 | 1168516, 13 | 5402628, 14 | 20776176, 15 | 45391616, 16 | 15139616, 17 | 64736, 18 | ] 19 | 20 | dists = [ 21 | 1, 22 | 18, 23 | 243, 24 | 3240, 25 | 43239, 26 | 574908, 27 | 7618438, 28 | 100803036, 29 | 1332343288, 30 | 17596479795, 31 | 232248063316, 32 | 3063288809012, 33 | 40374425656248, 34 | 531653418284628, 35 | 6989320578825358, 36 | 91365146187124320, 37 | 1100000000000000000, 38 | 12000000000000000000, 39 | 29000000000000000000, 40 | 1500000000000000000, 41 | 490000000, 42 | ] 43 | 44 | 45 | def uniform_random(frac): 46 | return random.uniform(0, 1) < frac 47 | 48 | 49 | def random_corner(half=False): 50 | random_number = random.randint(1, sum(CORNERS_DIST)) 51 | if half and uniform_random(1 - EXACT_FILLED): 52 | return 0 53 | cumulative_sum = 0 54 | for i, num in enumerate(CORNERS_DIST): 55 | cumulative_sum += num 56 | if random_number <= cumulative_sum: 57 | return i 58 | return len(CORNERS_DIST) - 1 59 | 60 | 61 | def actual_half_corner(): 62 | all_random_numbers = [random_corner(True) for _ in range(100000)] 63 | return sum(all_random_numbers) / len(all_random_numbers) 64 | 65 | 66 | def actual_approx_corner(): 67 | all_random_numbers = [] 68 | for _ in range(100000): 69 | frac, int_ = math.modf(1 / EXACT_FILLED) 70 | assert int_ != 0 71 | random_number = min(random_corner() for _ in range(int(int_))) 72 | if uniform_random(frac): 73 | random_number = min(random_number, random_corner()) 74 | 75 | all_random_numbers.append(random_number) 76 | return sum(all_random_numbers) / len(all_random_numbers) 77 | 78 | 79 | EXACT_FILLED = 0 80 | 81 | 82 | def main(): 83 | global EXACT_FILLED 84 | for _ in range(19): 85 | EXACT_FILLED += 0.05 86 | print( 87 | f"{EXACT_FILLED:.2f}: {actual_half_corner():.2f} {actual_approx_corner():.2f}" 88 | ) 89 | 90 | BRANCHING_FACTOR = 6 + math.sqrt(6) * 3 91 | SIZE = 43252003274489860000 92 | 93 | pos_at_depth = 1 94 | pos_seen = 1 95 | depth = 0 96 | 97 | while pos_seen < SIZE: 98 | print(f"{int(pos_at_depth)}\n{int(dists[depth])}\n") 99 | if pos_at_depth == 1: 100 | pos_at_depth = 18 101 | else: 102 | pos_at_depth *= BRANCHING_FACTOR * math.exp(-5.4768 * pos_seen / SIZE) 103 | pos_seen += pos_at_depth 104 | depth += 1 105 | 106 | 107 | if __name__ == "__main__": 108 | main() 109 | -------------------------------------------------------------------------------- /src/cycle_combination_solver/src/phase2/puzzle/cube3/mod.rs: -------------------------------------------------------------------------------- 1 | //! SIMD optimized implementations for 3x3 cubes 2 | 3 | #[cfg(not(any(avx2, simd8and16)))] 4 | pub type Cube3 = super::slice_puzzle::StackPuzzle<40>; 5 | 6 | mod common { 7 | use crate::phase2::puzzle::{KSolveConversionError, OrbitDef, OrientedPartition, PuzzleState}; 8 | use std::hash::Hash; 9 | use std::{fmt::Debug, num::NonZeroU8}; 10 | 11 | /// The interface for a 3x3 cube puzzle state 12 | pub trait Cube3Interface: Clone + PartialEq + Debug { 13 | /// Create a Cube3 state from a sorted list of move transformations. 14 | fn from_sorted_transformations(sorted_transformations: &[Vec<(u8, u8)>]) -> Self; 15 | 16 | /// Compose a and b into self. 17 | fn replace_compose(&mut self, a: &Self, b: &Self); 18 | 19 | /// Inverse a into self. 20 | fn replace_inverse(&mut self, a: &Self); 21 | 22 | /// Check if the cube induces a sorted cycle type. 23 | fn induces_sorted_cycle_type(&self, sorted_cycle_type: &[OrientedPartition; 2]) -> bool; 24 | 25 | /// Convert an orbit of the cube state into a pair of (perm, ori) bytes. 26 | /// For implementation reasons that should ideally be abstracted away, 27 | /// we have to make the arrays length 16. 28 | fn orbit_bytes(&self, orbit_index: usize) -> ([u8; 16], [u8; 16]); 29 | 30 | /// Exact hasher for an orbit. Note that this is different from a 31 | /// "hash", which in Rust terminology is something that implements Hash 32 | fn exact_hasher_orbit(&self, orbit_index: usize) -> u64; 33 | 34 | /// Approximate hash for an orbit 35 | fn approximate_hash_orbit(&self, orbit_index: usize) -> impl Hash; 36 | } 37 | 38 | pub const CUBE_3_SORTED_ORBIT_DEFS: [OrbitDef; 2] = [ 39 | OrbitDef { 40 | piece_count: NonZeroU8::new(8).unwrap(), 41 | orientation_count: NonZeroU8::new(3).unwrap(), 42 | }, 43 | OrbitDef { 44 | piece_count: NonZeroU8::new(12).unwrap(), 45 | orientation_count: NonZeroU8::new(2).unwrap(), 46 | }, 47 | ]; 48 | 49 | impl PuzzleState for C { 50 | type MultiBv = (); 51 | type OrbitBytesBuf<'a> 52 | = [u8; 16] 53 | where 54 | Self: 'a; 55 | 56 | fn new_multi_bv(_sorted_orbit_defs: &[OrbitDef]) { 57 | // Induces cycle type for 3x3 cubes doesn't require auxilliary 58 | // memory 59 | } 60 | 61 | fn try_from_transformation_meta( 62 | sorted_transformations: &[Vec<(u8, u8)>], 63 | sorted_orbit_defs: &[OrbitDef], 64 | ) -> Result { 65 | if sorted_orbit_defs == CUBE_3_SORTED_ORBIT_DEFS { 66 | Ok(Self::from_sorted_transformations(sorted_transformations)) 67 | } else { 68 | Err(KSolveConversionError::InvalidOrbitDefs { 69 | expected: CUBE_3_SORTED_ORBIT_DEFS.to_vec(), 70 | actual: sorted_orbit_defs.to_vec(), 71 | }) 72 | } 73 | } 74 | 75 | unsafe fn replace_compose(&mut self, a: &Self, b: &Self, _sorted_orbit_defs: &[OrbitDef]) { 76 | self.replace_compose(a, b); 77 | } 78 | 79 | fn replace_inverse(&mut self, a: &Self, _sorted_orbit_defs: &[OrbitDef]) { 80 | self.replace_inverse(a); 81 | } 82 | 83 | fn induces_sorted_cycle_type( 84 | &self, 85 | sorted_cycle_type: &[OrientedPartition], 86 | _sorted_orbit_defs: &[OrbitDef], 87 | _multi_bv: (), 88 | ) -> bool { 89 | // SAFETY: `try_from_transformation_meta`, the only constructor, 90 | // guarantees that this will always be length 2. 91 | // TODO: make sorted_cycle_type an actual type 92 | self.induces_sorted_cycle_type(unsafe { 93 | sorted_cycle_type.try_into().unwrap_unchecked() 94 | }) 95 | } 96 | 97 | fn next_orbit_identifer(orbit_identifier: usize, _orbit_def: OrbitDef) -> usize { 98 | orbit_identifier + 1 99 | } 100 | 101 | fn orbit_bytes( 102 | &self, 103 | orbit_identifier: usize, 104 | _orbit_def: OrbitDef, 105 | ) -> ([u8; 16], [u8; 16]) { 106 | self.orbit_bytes(orbit_identifier) 107 | } 108 | 109 | fn exact_hasher_orbit(&self, orbit_identifier: usize, _orbit_def: OrbitDef) -> u64 { 110 | // TODO: ghostcell trick to avoid the index check 111 | // TODO: make orbit_index an enum 112 | self.exact_hasher_orbit(orbit_identifier) 113 | } 114 | 115 | fn approximate_hash_orbit( 116 | &self, 117 | orbit_identifier: usize, 118 | _orbit_def: OrbitDef, 119 | ) -> impl Hash { 120 | self.approximate_hash_orbit(orbit_identifier) 121 | } 122 | } 123 | } 124 | 125 | pub(in crate::phase2::puzzle) mod avx2; 126 | pub(in crate::phase2::puzzle) mod simd8and16; 127 | 128 | #[cfg(avx2)] 129 | pub use avx2::Cube3; 130 | 131 | #[cfg(all(not(avx2), simd8and16))] 132 | pub use simd8and16::Cube3; 133 | 134 | // pub struct StackEvenCubeSimd { 135 | // cp: u8x8, 136 | // co: u8x8, 137 | // s_24s: [u8x32; S_24S], 138 | // } 139 | 140 | // pub struct HeapEvenCubeSimd { 141 | // cp: u8x8, 142 | // co: u8x8, 143 | // s_24s: [u8x32], 144 | // } 145 | 146 | // pub struct StackOddCubeSimd { 147 | // ep: u8x16, 148 | // eo: u8x16, 149 | // cp: u8x8, 150 | // co: u8x8, 151 | // s_24s: [u8x32; S_24S], 152 | // } 153 | 154 | // pub struct HeapOddCubeSimd { 155 | // cp: u8x8, 156 | // co: u8x8, 157 | // s_24s: [u8x32], 158 | // } 159 | -------------------------------------------------------------------------------- /src/interpreter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interpreter" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | qter_core = { path = "../qter_core" } 8 | internment = { version = "0.8", features = [ "arc" ] } 9 | compiler = { path = "../compiler" } 10 | 11 | [lints] 12 | workspace = true 13 | -------------------------------------------------------------------------------- /src/movecount_coefficient/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "movecount_coefficient" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | 8 | [lints] 9 | workspace = true 10 | -------------------------------------------------------------------------------- /src/movecount_coefficient/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic)] 2 | #![allow(unused_variables, dead_code)] 3 | 4 | //! A Rust port of the [Movecount Coefficient Calculator](https://trangium.github.io/MovecountCoefficient/) 5 | //! adapted with permission. 6 | 7 | // Very blatantly copy pasted from a single pass of AI transpilation 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq)] 10 | pub enum Location { 11 | Home, 12 | Top, 13 | Bottom, 14 | UFlick, 15 | DFlick, 16 | FFlick, 17 | SFlick, 18 | EFlick, 19 | MFlick, 20 | E, 21 | M, 22 | EidoFlick, 23 | LeftDb, 24 | RightDb, 25 | LeftU, 26 | RightU, 27 | RDown, 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct Finger { 32 | last_move_time: f64, 33 | location: Location, 34 | } 35 | 36 | impl Finger { 37 | fn new() -> Self { 38 | Self { 39 | last_move_time: -1.0, 40 | location: Location::Home, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug)] 46 | pub struct HandState { 47 | thumb: Finger, 48 | index: Finger, 49 | middle: Finger, 50 | ring: Finger, 51 | oh_cool: f64, 52 | wrist: i8, 53 | } 54 | 55 | impl HandState { 56 | fn new(wrist: i8) -> Self { 57 | Self { 58 | thumb: Finger::new(), 59 | index: Finger::new(), 60 | middle: Finger::new(), 61 | ring: Finger::new(), 62 | oh_cool: -1.0, 63 | wrist, 64 | } 65 | } 66 | 67 | fn max_finger_time(&self) -> f64 { 68 | self.thumb.last_move_time.max( 69 | self.index 70 | .last_move_time 71 | .max(self.middle.last_move_time.max(self.ring.last_move_time)), 72 | ) 73 | } 74 | } 75 | 76 | #[derive(Debug)] 77 | pub struct AlgSpeedConfig { 78 | ignore_errors: bool, 79 | ignore_auf: bool, 80 | wrist_mult: f64, 81 | push_mult: f64, 82 | ring_mult: f64, 83 | destabilize: f64, 84 | add_regrip: f64, 85 | double: f64, 86 | seslice_mult: f64, 87 | over_work_mult: f64, 88 | move_block: f64, 89 | rotation: f64, 90 | } 91 | 92 | impl Default for AlgSpeedConfig { 93 | fn default() -> Self { 94 | Self { 95 | ignore_errors: false, 96 | ignore_auf: false, 97 | wrist_mult: 0.8, 98 | push_mult: 1.3, 99 | ring_mult: 1.4, 100 | destabilize: 0.5, 101 | add_regrip: 1.0, 102 | double: 1.65, 103 | seslice_mult: 1.25, 104 | over_work_mult: 2.25, 105 | move_block: 0.8, 106 | rotation: 3.5, 107 | } 108 | } 109 | } 110 | 111 | pub struct AlgSpeed { 112 | config: AlgSpeedConfig, 113 | } 114 | 115 | impl AlgSpeed { 116 | #[must_use] 117 | pub fn new(config: AlgSpeedConfig) -> Self { 118 | Self { config } 119 | } 120 | 121 | fn calc_overwork( 122 | &self, 123 | finger: &Finger, 124 | location_prefer: Location, 125 | penalty: f64, 126 | speed: f64, 127 | ) -> f64 { 128 | if finger.location != location_prefer && speed - finger.last_move_time < penalty { 129 | penalty - speed + finger.last_move_time 130 | } else { 131 | 0.0 132 | } 133 | } 134 | 135 | fn process_sequence(&self, sequence: &str) -> Result { 136 | let split_seq: Vec<&str> = sequence.split_whitespace().collect(); 137 | let true_split_seq: Vec = if self.config.ignore_errors { 138 | split_seq 139 | .into_iter() 140 | .filter(|&move_str| { 141 | let valid_moves = [ 142 | "r", "r2", "r'", "u", "u'", "u2", "f", "f2", "f'", "d", "d2", "d'", "l", 143 | "l2", "l'", "b", "b2", "b'", "m", "m2", "m'", "s", "s2", "s'", "e", "e2", 144 | "e'", "x", "x'", "x2", "y", "y'", "y2", "z", "z'", "z2", 145 | ]; 146 | valid_moves.contains(&move_str.to_lowercase().as_str()) 147 | }) 148 | .map(String::from) 149 | .collect() 150 | } else { 151 | split_seq.into_iter().map(String::from).collect() 152 | }; 153 | 154 | let mut final_seq = true_split_seq; 155 | 156 | if self.config.ignore_auf { 157 | // Handle AUF at start 158 | if !final_seq.is_empty() { 159 | if final_seq[0].starts_with('U') { 160 | final_seq.remove(0); 161 | } else if final_seq.len() >= 2 162 | && final_seq[0].to_lowercase().starts_with('d') 163 | && final_seq[1].starts_with('U') 164 | { 165 | final_seq[1] = final_seq[0].clone(); 166 | final_seq.remove(0); 167 | } 168 | } 169 | 170 | // Handle AUF at end 171 | if !final_seq.is_empty() { 172 | let last_idx = final_seq.len() - 1; 173 | if final_seq[last_idx].starts_with('U') { 174 | final_seq.pop(); 175 | } else if final_seq.len() >= 2 { 176 | let second_last_idx = final_seq.len() - 2; 177 | if final_seq[last_idx].to_lowercase().starts_with('d') 178 | && final_seq[second_last_idx].starts_with('U') 179 | { 180 | final_seq[second_last_idx] = final_seq[last_idx].clone(); 181 | final_seq.pop(); 182 | } 183 | } 184 | } 185 | } 186 | 187 | let initial_tests = vec![ 188 | self.test_sequence(&final_seq, 0, 0, 0.0), 189 | self.test_sequence(&final_seq, 0, -1, 1.0 + self.config.add_regrip), 190 | self.test_sequence(&final_seq, 0, 1, 1.0 + self.config.add_regrip), 191 | self.test_sequence(&final_seq, -1, 0, 1.0 + self.config.add_regrip), 192 | self.test_sequence(&final_seq, 1, 0, 1.0 + self.config.add_regrip), 193 | ]; 194 | 195 | self.find_best_speed(initial_tests, &final_seq) 196 | } 197 | 198 | fn test_sequence( 199 | &self, 200 | sequence: &[String], 201 | l_grip: i8, 202 | r_grip: i8, 203 | initial_speed: f64, 204 | ) -> TestResult { 205 | let mut left = HandState::new(l_grip); 206 | let mut right = HandState::new(r_grip); 207 | let mut speed = initial_speed; 208 | let mut grip = 1; 209 | let mut ud_grip = -1; 210 | // let mut prev_speed = None; 211 | // let mut first_move_speed = None; 212 | 213 | for (i, move_str) in sequence.iter().enumerate() { 214 | // Process move logic here... 215 | // This would be a very large match statement handling all possible moves 216 | // Similar to the JavaScript switch statement but in Rust style 217 | } 218 | 219 | TestResult { 220 | move_index: -1, 221 | speed, 222 | left_wrist: l_grip, 223 | right_wrist: r_grip, 224 | left_time: left.max_finger_time(), 225 | right_time: right.max_finger_time(), 226 | } 227 | } 228 | 229 | fn find_best_speed( 230 | &self, 231 | initial_tests: Vec, 232 | sequence: &[String], 233 | ) -> Result { 234 | // Implementation of the speed finding algorithm 235 | // This would replace the while(true) loop from JavaScript 236 | Ok(0.0) // Placeholder 237 | } 238 | } 239 | 240 | #[derive(Debug)] 241 | struct TestResult { 242 | move_index: i32, 243 | speed: f64, 244 | left_wrist: i8, 245 | right_wrist: i8, 246 | left_time: f64, 247 | right_time: f64, 248 | } 249 | -------------------------------------------------------------------------------- /src/pog_ans/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pog_ans" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | num-traits = "0.2" 8 | 9 | [lints] 10 | workspace = true 11 | -------------------------------------------------------------------------------- /src/puzzle_geometry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "puzzle_geometry" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [dependencies] 10 | qter_core = { path = "../qter_core" } 11 | phf = { version = "0.11.3", features = ["macros"] } 12 | internment = { version = "0.8", features = ["arc"] } 13 | nalgebra = "0.33" 14 | thiserror = "2.0" 15 | itertools = "0.13" 16 | -------------------------------------------------------------------------------- /src/puzzle_geometry/src/edge_cloud.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, mem}; 2 | 3 | use itertools::Itertools; 4 | use nalgebra::{Matrix3, Vector3}; 5 | 6 | use crate::E; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct EdgeCloud { 10 | sections: Vec, Vector3)>>, 11 | } 12 | 13 | impl EdgeCloud { 14 | pub fn new(mut sections: Vec, Vector3)>>) -> EdgeCloud { 15 | for section in &mut sections { 16 | sort_edge_cloud(section); 17 | } 18 | 19 | EdgeCloud { sections } 20 | } 21 | 22 | pub fn try_symmetry(&self, into: &mut EdgeCloud, matrix: Matrix3) -> bool { 23 | self.sections 24 | .iter() 25 | .zip(into.sections.iter_mut()) 26 | .all(|(section, into)| try_symmetry(section, into, matrix)) 27 | } 28 | 29 | pub fn epsilon_eq(&self, other: &EdgeCloud) -> bool { 30 | if self.sections.len() != other.sections.len() { 31 | return false; 32 | } 33 | 34 | self.sections 35 | .iter() 36 | .zip(other.sections.iter()) 37 | .all(|(section, other)| edge_cloud_eq(section, other)) 38 | } 39 | 40 | pub fn sections(&self) -> &[Vec<(Vector3, Vector3)>] { 41 | &self.sections 42 | } 43 | } 44 | 45 | fn try_symmetry( 46 | cloud: &[(Vector3, Vector3)], 47 | into: &mut [(Vector3, Vector3)], 48 | matrix: Matrix3, 49 | ) -> bool { 50 | into.copy_from_slice(cloud); 51 | 52 | for point in into.iter_mut().flat_map(|(a, b)| [a, b]) { 53 | *point = matrix * *point; 54 | } 55 | 56 | sort_edge_cloud(into); 57 | 58 | edge_cloud_eq(cloud, into) 59 | } 60 | 61 | fn sort_edge_cloud(cloud: &mut [(Vector3, Vector3)]) { 62 | for (a, b) in &mut *cloud { 63 | let ordering = a 64 | .iter() 65 | .zip(b.iter()) 66 | .map(|(x1, x2)| { 67 | if (x1 - x2).abs() < E { 68 | return Ordering::Equal; 69 | } 70 | 71 | x1.total_cmp(x2) 72 | }) 73 | .find_or_last(|v| !matches!(v, Ordering::Equal)) 74 | .unwrap(); 75 | 76 | if matches!(ordering, Ordering::Greater) { 77 | mem::swap(a, b); 78 | } 79 | } 80 | 81 | cloud.sort_unstable_by(|(a1, b1), (a2, b2)| { 82 | a1.as_slice() 83 | .iter() 84 | .zip(a2.as_slice().iter()) 85 | .chain(b1.as_slice().iter().zip(b2.as_slice().iter())) 86 | .map(|(x1, x2)| { 87 | if (x1 - x2).abs() < E { 88 | return Ordering::Equal; 89 | } 90 | 91 | x1.total_cmp(x2) 92 | }) 93 | .find_or_last(|v| !matches!(v, Ordering::Equal)) 94 | .unwrap() 95 | }); 96 | } 97 | 98 | fn edge_cloud_eq( 99 | cloud1: &[(Vector3, Vector3)], 100 | cloud2: &[(Vector3, Vector3)], 101 | ) -> bool { 102 | cloud1 103 | .iter() 104 | .zip(cloud2) 105 | .all(|((a1, b1), (a2, b2))| (a1.metric_distance(a2) < E) && (b1.metric_distance(b2) < E)) 106 | } 107 | -------------------------------------------------------------------------------- /src/puzzle_geometry/src/shapes.rs: -------------------------------------------------------------------------------- 1 | use crate::{Face, Point, Polyhedron, PuzzleDescriptionString}; 2 | use internment::ArcIntern; 3 | use nalgebra::{Rotation3, Unit, Vector3}; 4 | use std::sync::LazyLock; 5 | 6 | pub static TETRAHEDRON: LazyLock = LazyLock::new(|| { 7 | let up = Point(Vector3::new(0., 1., 0.)); 8 | 9 | let down_1 = Point( 10 | Rotation3::from_axis_angle( 11 | &Unit::new_normalize(Vector3::new(1., 0., 0.)), 12 | (-1. / 3_f64).acos(), 13 | ) * up.0, 14 | ); 15 | let down_2 = down_1.rotated(Vector3::new(0., 1., 0.), 3); 16 | let down_3 = down_2.rotated(Vector3::new(0., 1., 0.), 3); 17 | 18 | Polyhedron(vec![ 19 | Face { 20 | points: vec![up, down_1, down_2], 21 | color: ArcIntern::from("green"), 22 | }, 23 | Face { 24 | points: vec![up, down_2, down_3], 25 | color: ArcIntern::from("blue"), 26 | }, 27 | Face { 28 | points: vec![up, down_3, down_1], 29 | color: ArcIntern::from("yellow"), 30 | }, 31 | Face { 32 | points: vec![down_1, down_2, down_3], 33 | color: ArcIntern::from("red"), 34 | }, 35 | ]) 36 | }); 37 | 38 | pub static CUBE: LazyLock = LazyLock::new(|| { 39 | let up = Face { 40 | points: vec![ 41 | Point(Vector3::new(1., 1., 1.)), 42 | Point(Vector3::new(-1., 1., 1.)), 43 | Point(Vector3::new(-1., 1., -1.)), 44 | Point(Vector3::new(1., 1., -1.)), 45 | ], 46 | color: ArcIntern::from("white"), 47 | }; 48 | 49 | let mut right = up.clone().rotated(Vector3::new(1., 0., 0.), 4); 50 | right.color = ArcIntern::from("red"); 51 | let mut down = right.clone().rotated(Vector3::new(1., 0., 0.), 4); 52 | down.color = ArcIntern::from("yellow"); 53 | let mut left = down.clone().rotated(Vector3::new(1., 0., 0.), 4); 54 | left.color = ArcIntern::from("orange"); 55 | 56 | let mut front = up.clone().rotated(Vector3::new(0., 0., 1.), 4); 57 | front.color = ArcIntern::from("green"); 58 | let mut back = up.clone().rotated(Vector3::new(0., 0., -1.), 4); 59 | back.color = ArcIntern::from("blue"); 60 | 61 | Polyhedron(vec![up, right, down, left, front, back]) 62 | }); 63 | 64 | pub static SHAPES: phf::Map<&'static str, &LazyLock> = phf::phf_map! { 65 | "c" => &CUBE, 66 | "t" => &TETRAHEDRON, 67 | }; 68 | 69 | pub static PUZZLES: phf::Map<&'static str, PuzzleDescriptionString> = phf::phf_map! { 70 | "2x2x2" => "c f 0", 71 | "3x3x3" => "c f 0.333333333333333", 72 | "4x4x4" => "c f 0.5 f 0", 73 | "5x5x5" => "c f 0.6 f 0.2", 74 | "6x6x6" => "c f 0.666666666666667 f 0.333333333333333 f 0", 75 | "7x7x7" => "c f 0.714285714285714 f 0.428571428571429 f 0.142857142857143", 76 | "8x8x8" => "c f 0.75 f 0.5 f 0.25 f 0", 77 | "9x9x9" => "c f 0.777777777777778 f 0.555555555555556 f 0.333333333333333 f 0.111111111111111", 78 | "10x10x10" => "c f 0.8 f 0.6 f 0.4 f 0.2 f 0", 79 | "11x11x11" => "c f 0.818181818181818 f 0.636363636363636 f 0.454545454545455 f 0.272727272727273 f 0.0909090909090909", 80 | "12x12x12" => "c f 0.833333333333333 f 0.666666666666667 f 0.5 f 0.333333333333333 f 0.166666666666667 f 0", 81 | "13x13x13" => "c f 0.846153846153846 f 0.692307692307692 f 0.538461538461538 f 0.384615384615385 f 0.230769230769231 f 0.0769230769230769", 82 | "20x20x20" => "c f 0 f .1 f .2 f .3 f .4 f .5 f .6 f .7 f .8 f .9", 83 | "30x30x30" => "c f 0 f .066667 f .133333 f .2 f .266667 f .333333 f .4 f .466667 f .533333 f .6 f .666667 f .733333 f .8 f .866667 f .933333", 84 | "40x40x40" => "c f 0 f .05 f .1 f .15 f .2 f .25 f .3 f .35 f .4 f .45 f .5 f .55 f .6 f .65 f .7 f .75 f .8 f .85 f .9 f .95", 85 | "skewb" => "c v 0", 86 | "master skewb" => "c v 0.275", 87 | "professor skewb" => "c v 0 v 0.38", 88 | "compy cube" => "c v 0.915641442663986", 89 | "helicopter" => "c e 0.707106781186547", 90 | "curvy copter" => "c e 0.83", 91 | "dino" => "c v 0.577350269189626", 92 | "little chop" => "c e 0", 93 | "pyramorphix" => "t e 0", 94 | "mastermorphix" => "t e 0.346184634065199", 95 | "pyraminx" => "t v 0.333333333333333 v 1.66666666666667", 96 | "tetraminx" => "t v 0.333333333333333", 97 | "master pyraminx" => "t v 0 v 1 v 2", 98 | "master tetraminx" => "t v 0 v 1", 99 | "professor pyraminx" => "t v -0.2 v 0.6 v 1.4 v 2.2", 100 | "professor tetraminx" => "t v -0.2 v 0.6 v 1.4", 101 | "royal pyraminx" => "t v -0.333333333333333 v 0.333333333333333 v 1 v 1.66666666666667 v 2.33333333333333", 102 | "royal tetraminx" => "t v -0.333333333333333 v 0.333333333333333 v 1 v 1.66666666666667", 103 | "emperor pyraminx" => "t v -0.428571428571429 v 0.142857142857143 v 0.714285714285714 v 1.28571428571429 v 1.85714285714286 v 2.42857142857143", 104 | "emperor tetraminx" => "t v -0.428571428571429 v 0.142857142857143 v 0.714285714285714 v 1.28571428571429 v 1.85714285714286", 105 | "Jing pyraminx" => "t f 0", 106 | "master pyramorphix" => "t e 0.866025403784437", 107 | "megaminx" => "d f 0.7", 108 | "gigaminx" => "d f 0.64 f 0.82", 109 | "teraminx" => "d f 0.64 f 0.76 f 0.88", 110 | "petaminx" => "d f 0.64 f 0.73 f 0.82 f 0.91", 111 | "examinx" => "d f 0.64 f 0.712 f 0.784 f 0.856 f 0.928", 112 | "zetaminx" => "d f 0.64 f 0.7 f 0.76 f 0.82 f 0.88 f 0.94", 113 | "yottaminx" => "d f 0.64 f 0.6914 f 0.7429 f 0.7943 f 0.8457 f 0.8971 f 0.9486", 114 | "pentultimate" => "d f 0", 115 | "master pentultimate" => "d f 0.1", 116 | "elite pentultimate" => "d f 0 f 0.145905", 117 | "starminx" => "d v 0.937962370425399", // sqrt(5(5-2 sqrt(5))/3) 118 | "starminx 2" => "d f 0.23606797749979", 119 | "pyraminx crystal" => "d f 0.447213595499989", 120 | "chopasaurus" => "d v 0", 121 | "big chop" => "d e 0", 122 | "skewb diamond" => "o f 0", 123 | "FTO" => "o f 0.333333333333333", 124 | "master FTO" => "o f 0.5 f 0", 125 | "Christopher's jewel" => "o v 0.577350269189626", 126 | "octastar" => "o e 0", 127 | "Trajber's octahedron" => "o v 0.433012701892219", 128 | "radio chop" => "i f 0", 129 | "icosamate" => "i v 0", 130 | "Regular Astrominx" => "i v 0.18759247376021", 131 | "Regular Astrominx + Big Chop" => "i v 0.18759247376021 e 0", 132 | "Redicosahedron" => "i v 0.794654472291766", 133 | "Redicosahedron with centers" => "i v 0.84", 134 | "Icosaminx" => "i v 0.73", 135 | "Eitan's star" => "i f 0.61803398874989", 136 | "2x2x2 + dino" => "c f 0 v 0.577350269189626", 137 | "2x2x2 + little chop" => "c f 0 e 0", 138 | "dino + little chop" => "c v 0.577350269189626 e 0", 139 | "2x2x2 + dino + little chop" => "c f 0 v 0.577350269189626 e 0", 140 | "megaminx + chopasaurus" => "d f 0.61803398875 v 0", 141 | "starminx combo" => "d f 0.23606797749979 v 0.937962370425399", 142 | }; 143 | -------------------------------------------------------------------------------- /src/qter_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qter_core" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | bnum = "0.12" 8 | itertools = "0.14" 9 | pest = "2.7" 10 | pest_derive = "2.7" 11 | internment = { version = "0.8", features = [ "arc" ] } 12 | pog_ans = { path = "../pog_ans" } 13 | 14 | [lints] 15 | workspace = true 16 | -------------------------------------------------------------------------------- /src/qter_core/prelude.qat: -------------------------------------------------------------------------------- 1 | .start-lua 2 | local function validate_equals_order(r1, r2, r3) 3 | if order_of_reg(r3) < order_of_reg(r1) or 4 | order_of_reg(r3) < order_of_reg(r2) then 5 | error("Register 3 must not be the minimum order (" .. 6 | order_of_reg(r3) .. " < min(" .. order_of_reg(r1) .. ", " .. 7 | order_of_reg(r2) .. ")" 8 | ) 9 | end 10 | 11 | -- functions that do not output qat must otherwise return nil 12 | return nil 13 | end 14 | 15 | function validate_same_orders(...) 16 | local args = {...} 17 | local first = order_of_reg(args[1]) 18 | 19 | for i = 2, #args do 20 | if order_of_reg(args[i]) ~= first then 21 | error("Arguments are not all the same value") 22 | end 23 | end 24 | 25 | return nil 26 | end 27 | 28 | function subtract_order_relative(r1, n) 29 | return { { "add", r1, order_of_reg(r1) - n } } 30 | end 31 | end-lua 32 | 33 | .macro sub { 34 | ($R:reg $N:int) => lua subtract_order_relative($R, $N) 35 | } 36 | 37 | .macro inc { 38 | ($R:reg) => add $R 1 39 | } 40 | 41 | .macro dec { 42 | ($R:reg) => sub $R 1 43 | } 44 | 45 | .macro move { 46 | ($R1:reg to $R2:reg) => { 47 | -- TODO: lua to optimize moves using register modulus 48 | zero_loop: 49 | solved-goto $R1 move_done 50 | dec $R1 51 | inc $R2 52 | goto zero_loop 53 | move_done: 54 | } 55 | } 56 | 57 | .macro set { 58 | ($R1:reg to $R2:reg) => { 59 | set $R1 to 0 60 | move $R2 to $R1 61 | } 62 | 63 | ($R:reg to $N:int) => { 64 | zero_loop: 65 | solved-goto $R move_done 66 | dec $R 67 | goto zero_loop 68 | move_done: 69 | add $R $N 70 | } 71 | } 72 | 73 | .macro if { 74 | (solved $R:reg $code:block) => { 75 | solved-goto $R do_if 76 | goto after_if 77 | do_if: 78 | $code 79 | after_if: 80 | } 81 | (not-solved $R:reg $code:block) => { 82 | solved-goto $R after_if 83 | $code 84 | after_if: 85 | } 86 | (solved $R:reg $code1:block else $code2:block) => { 87 | if not-solved $R { 88 | $code2 89 | } else { 90 | $code1 91 | } 92 | } 93 | (not-solved $R:reg $code1:block else $code2:block) => { 94 | if not-solved $R { 95 | $code1 96 | goto over_else 97 | } 98 | $code2 99 | over_else: 100 | } 101 | (equals $R:reg $N:int $code:block) => { 102 | if equals $R $N { 103 | $code 104 | } else {} 105 | } 106 | (not-equals $R:reg $N:int $code:block) => { 107 | if not-equals $R $N { 108 | $code 109 | } else {} 110 | } 111 | (equals $R:reg $N:int $code1:block else $code2:block) => { 112 | if not-equals $R $N { 113 | $code2 114 | } else { 115 | $code1 116 | } 117 | } 118 | (not-equals $R:reg $N:int $code1:block else $code2:block) => { 119 | sub $R $N 120 | if not-solved $R { 121 | add $R $N 122 | $code2 123 | } else { 124 | add $R $N 125 | $code1 126 | } 127 | } 128 | (equals $R1:reg $R2:reg using $R3:reg $code:block) => { 129 | lua validate_equals_order($R1, $R2, $R3) 130 | set $R3 to 0 131 | equals_loop: 132 | solved-goto $R1 first_zero 133 | solved-goto $R2 fail_loop 134 | dec $R1 135 | dec $R2 136 | inc $R3 137 | goto equals_loop 138 | first_zero: 139 | solved-goto $R2 success_loop 140 | fail_loop: 141 | solved-goto $R3 fail 142 | inc $R1 143 | inc $R2 144 | dec $R3 145 | goto fail_loop 146 | success_loop: 147 | solved-goto $R3 success 148 | inc $R1 149 | inc $R2 150 | dec $R3 151 | goto success_loop 152 | success: 153 | $code 154 | fail: 155 | } 156 | (not-equals $R1:reg $R2:reg using $R3:reg $code:block) => { 157 | lua validate_equals_order($R1, $R2, $R3) 158 | set $R3 to 0 159 | equals_loop: 160 | solved-goto $R1 first_zero 161 | solved-goto $R2 fail_loop 162 | dec $R1 163 | dec $R2 164 | inc $R3 165 | goto equals_loop 166 | first_zero: 167 | solved-goto $R2 success_loop 168 | fail_loop: 169 | solved-goto $R3 fail 170 | inc $R1 171 | inc $R2 172 | dec $R3 173 | goto fail_loop 174 | success_loop: 175 | solved-goto $R3 success 176 | inc $R1 177 | inc $R2 178 | dec $R3 179 | goto success_loop 180 | -- Only change is to swap these two labels 181 | fail: 182 | $code 183 | success: 184 | } 185 | (equals $R1:reg $R2:reg using $R3:reg $code1:block else $code2:block) => { 186 | if equals $R1 $R2 using $R3 { 187 | $code1 188 | goto over_else 189 | } 190 | $code2 191 | over_else: 192 | } 193 | (not-equals $R1:reg $R2:reg using $R3:reg $code1:block else $code2:block) => { 194 | if equals $R1 $R2 using $R3 { 195 | $code2 196 | } else { 197 | $code1 198 | } 199 | } 200 | } 201 | 202 | .macro loop { 203 | ($code:block) => { 204 | !continue: 205 | $code 206 | goto continue 207 | !break: 208 | } 209 | } 210 | 211 | .macro while { 212 | (solved $R:reg $code:block) => { 213 | goto continue 214 | do_iteration: 215 | $code 216 | !continue: 217 | solved-goto $R do_iteration 218 | !break: 219 | } 220 | (not-solved $R:reg $code:block) => { 221 | !continue: 222 | solved-goto $R break 223 | $code 224 | goto continue 225 | !break: 226 | } 227 | (equals $R:reg $N:int $code:block) => { 228 | loop { 229 | if not-equals $R $N { 230 | goto break 231 | } 232 | $code 233 | } 234 | } 235 | (not-equals $R:reg $N:int $code:block) => { 236 | loop { 237 | if equals $R $N { 238 | goto break 239 | } 240 | $code 241 | } 242 | } 243 | (equals $R1:reg $R2:reg using $R3:reg $code:block) => { 244 | loop { 245 | if not-equals $R1 $R2 using $R3 { 246 | goto break 247 | } 248 | $code 249 | } 250 | } 251 | (not-equals $R1:reg $R2:reg using $R3:reg $code:block) => { 252 | loop { 253 | if equals $R1 $R2 using $R3 { 254 | goto break 255 | } 256 | $code 257 | } 258 | } 259 | } 260 | 261 | .start-lua 262 | -- `repeat` is a lua keyword 263 | function repeat_(code, start, end_, var) 264 | local output = {} 265 | 266 | for i = start, end_ do 267 | if var ~= nil then 268 | output[#output + 1] = { ".define", "$" .. var, i } 269 | end 270 | output[#output + 1] = { code } 271 | end 272 | 273 | return output 274 | end 275 | end-lua 276 | 277 | .macro repeat { 278 | ($times:int $code:block) => lua repeat_($code, 1, $times) 279 | 280 | ($times:int $var:ident $code:block) => lua repeat_($code, 1, $times, $var) 281 | 282 | -- should parse nested for loops such as `repeat j from $i to 10 { ... }` 283 | -- because the outer $i should be evaluated first 284 | ($times:int $var:ident from $start:int to $end:int $code:block) => { 285 | lua repeat_($code, $start, $end, $var) 286 | } 287 | } 288 | 289 | .start-lua 290 | function multiply(r1, r2, r3) 291 | validate_same_orders(r1, r2, r3) 292 | local output = {} 293 | -- TODO: Implement multiplication. Result in register r1, nullify r2/r3 294 | return output 295 | end 296 | end-lua 297 | 298 | .macro multiply { 299 | ($R1:reg $R2:reg using $R3:reg) => { 300 | lua multiply($R1, $R2, $R3) 301 | } 302 | ($R1:reg $N:int at $R2:reg) => { 303 | set $R2 to 0 304 | multiply_loop: 305 | solved-goto $R1 multiply_done 306 | add $R2 N 307 | dec $R1 308 | goto multiply_loop 309 | multiply_done: 310 | } 311 | } 312 | 313 | -------------------------------------------------------------------------------- /src/qter_core/puzzles/210-24.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/src/qter_core/puzzles/210-24.bin -------------------------------------------------------------------------------- /src/qter_core/puzzles/30-30-30.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArhanChaudhary/qter/d6101109d9da57eea0c5310151223b9982b2839b/src/qter_core/puzzles/30-30-30.bin -------------------------------------------------------------------------------- /src/qter_core/puzzles/3x3.txt: -------------------------------------------------------------------------------- 1 | COLORS 2 | 3 | White -> 1, 2, 3, 4, 5, 6, 7, 8 4 | Green -> 17, 18, 19, 20, 21, 22, 23, 24 5 | Red -> 25, 26, 27, 28, 29, 30, 31, 32 6 | Blue -> 33, 34, 35, 36, 37, 38, 39, 40 7 | Orange -> 9, 10, 11, 12, 13, 14, 15, 16 8 | Yellow -> 41, 42, 43, 44, 45, 46, 47, 48 9 | 10 | GENERATORS 11 | 12 | U = ( 1, 3, 8, 6)( 2, 5, 7, 4)( 9,33,25,17)(10,34,26,18)(11,35,27,19) 13 | L = ( 9,11,16,14)(10,13,15,12)( 1,17,41,40)( 4,20,44,37)( 6,22,46,35) 14 | F = (17,19,24,22)(18,21,23,20)( 6,25,43,16)( 7,28,42,13)( 8,30,41,11) 15 | R = (25,27,32,30)(26,29,31,28)( 3,38,43,19)( 5,36,45,21)( 8,33,48,24) 16 | B = (33,35,40,38)(34,37,39,36)( 3, 9,46,32)( 2,12,47,29)( 1,14,48,27) 17 | D = (41,43,48,46)(42,45,47,44)(14,22,30,38)(15,23,31,39)(16,24,32,40) 18 | 19 | DERIVED 20 | 21 | U2 = U U 22 | L2 = L L 23 | F2 = F F 24 | R2 = R R 25 | B2 = B B 26 | D2 = D D 27 | 28 | U' = U U U 29 | L' = L L L 30 | F' = F F F 31 | R' = R R R 32 | B' = B B B 33 | D' = D D D 34 | 35 | PRESETS 36 | 37 | (1260) R U2 D' B D' 38 | (4, 4) U / D 39 | (90, 90) R' F' L U' L U L F U' R / U F R' D' R2 F R' U' D 40 | (210, 24) U R U' D2 B / B U2 B' L' U2 B U L' B L B2 L ~ 0 41 | (30, 30, 30) U L2 B' L U' B' U2 R B' R' B L / R2 L U' R' L2 F' D R' D L B2 D2 / L2 F2 U L' F D' F' U' L' F U D L' U' ~ 1 42 | (30, 18, 10, 9) U L B' L B' U R' D U2 L2 F2 / D L' F L2 B L' F' L B' D' L' / R' U' L' F2 L F U F R L U' / B2 U2 L F' R B L2 D2 B R' F L 43 | -------------------------------------------------------------------------------- /src/qter_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic)] 2 | #![allow(clippy::too_many_lines, clippy::similar_names)] 3 | 4 | pub mod architectures; 5 | mod puzzle_parser; 6 | mod shared_facelet_detection; 7 | pub mod table_encoding; 8 | 9 | mod span; 10 | pub use span::*; 11 | 12 | mod runtime; 13 | pub use runtime::*; 14 | 15 | mod math; 16 | pub use math::*; 17 | -------------------------------------------------------------------------------- /src/qter_core/src/math/discrete_math.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | I, Int, U, 3 | architectures::{Algorithm, Permutation}, 4 | }; 5 | 6 | /// Calculate the GCD of two numbers 7 | #[must_use] 8 | pub fn gcd(mut a: Int, mut b: Int) -> Int { 9 | loop { 10 | if b.is_zero() { 11 | return a; 12 | } 13 | 14 | let rem = a % b; 15 | a = b; 16 | b = rem; 17 | } 18 | } 19 | 20 | /// Calculate the LCM of two numbers 21 | /// 22 | /// # Panics 23 | /// 24 | /// Panics if either number is zero. 25 | #[must_use] 26 | pub fn lcm(a: Int, b: Int) -> Int { 27 | assert!(!a.is_zero()); 28 | assert!(!b.is_zero()); 29 | 30 | b / gcd(a, b) * a 31 | } 32 | 33 | /// Calculate the LCM of a list of numbers 34 | pub fn lcm_iter(values: impl Iterator>) -> Int { 35 | values.fold(Int::one(), lcm) 36 | } 37 | 38 | /// Calculate the GCD of two numbers as well as the coefficients of Bézout's identity 39 | #[must_use] 40 | pub fn extended_euclid(mut a: Int, mut b: Int) -> ((Int, Int), Int) { 41 | let mut a_coeffs = (Int::::one(), Int::::zero()); 42 | let mut b_coeffs = (Int::::zero(), Int::::one()); 43 | 44 | loop { 45 | if b.is_zero() { 46 | return (a_coeffs, a); 47 | } 48 | 49 | let to_sub = a / b; 50 | let new_coeffs = ( 51 | a_coeffs.0 - b_coeffs.0 * to_sub, 52 | a_coeffs.1 - b_coeffs.1 * to_sub, 53 | ); 54 | let rem = a - b * to_sub; 55 | a = b; 56 | a_coeffs = b_coeffs; 57 | b = rem; 58 | b_coeffs = new_coeffs; 59 | } 60 | } 61 | 62 | // Implementation based on https://math.stackexchange.com/questions/1644677/what-to-do-if-the-modulus-is-not-coprime-in-the-chinese-remainder-theorem 63 | /// Calculate the chinese remainder theorem over a list of tuples of remainders with moduli. The return value is bounded by the LCM of the moduli. 64 | /// 65 | /// For each `(k, m) ∈ conditions`, the return value is congruent to `k mod m`. 66 | /// 67 | /// This is a generalization of the CRT that supports moduli that aren't coprime. Because of this, a value that satifies all of the conditions is not guaranteed. If the conditions contradict each other, the function will return `None`. 68 | /// 69 | /// If any of the conditions give `None`, the function will stop and return `None`. 70 | pub fn chinese_remainder_theorem( 71 | mut conditions: impl Iterator, Int)>>, 72 | ) -> Option> { 73 | let (mut prev_remainder, mut prev_modulus) = match conditions.next() { 74 | Some(Some(condition)) => condition, 75 | Some(None) => return None, 76 | None => return Some(Int::::zero()), 77 | }; 78 | 79 | for cond in conditions { 80 | let (remainder, modulus) = cond?; 81 | 82 | let (coeffs, gcd) = extended_euclid(prev_modulus, modulus); 83 | 84 | let diff = if remainder > prev_remainder { 85 | remainder - prev_remainder 86 | } else { 87 | prev_remainder - remainder 88 | }; 89 | 90 | if !(diff % gcd).is_zero() { 91 | return None; 92 | } 93 | 94 | let λ = diff / gcd; 95 | 96 | let x = if remainder > prev_remainder { 97 | remainder - modulus * coeffs.1 * λ 98 | } else { 99 | prev_remainder - prev_modulus * coeffs.0 * λ 100 | }; 101 | 102 | let new_modulus = lcm(prev_modulus, modulus); 103 | 104 | prev_remainder = x % new_modulus; 105 | prev_modulus = new_modulus; 106 | } 107 | 108 | Some(prev_remainder) 109 | } 110 | 111 | /// This function does what it says on the tin. 112 | /// 113 | /// "AAAA" → 1 114 | /// "ABAB" → 2 115 | /// "ABCA" → 4 116 | /// "ABABA" → 5 117 | /// 118 | /// Every string given by the iterator is treated as a unit rather than split apart, so `["Yellow", "Green", "Yellow", "Green"]` would return `2`. 119 | /// 120 | /// This function is important for computing the chromatic order of cycles. 121 | pub fn length_of_substring_that_this_string_is_n_repeated_copies_of<'a>( 122 | colors: impl Iterator, 123 | ) -> usize { 124 | let mut found = vec![]; 125 | let mut current_repeat_length = 1; 126 | 127 | for (i, color) in colors.enumerate() { 128 | found.push(color); 129 | 130 | if found[i % current_repeat_length] != color { 131 | current_repeat_length = i + 1; 132 | } 133 | } 134 | 135 | // We didn't match the substring a whole number of times; it actually doesn't work 136 | if found.len() % current_repeat_length != 0 { 137 | current_repeat_length = found.len(); 138 | } 139 | 140 | current_repeat_length 141 | } 142 | 143 | /// Decode the permutation using the register generator and the given facelets. 144 | /// 145 | /// In general, an arbitrary scramble cannot be decoded. If this is the case, the function will return `None`. 146 | pub fn decode( 147 | permutation: &Permutation, 148 | facelets: &[usize], 149 | generator: &Algorithm, 150 | ) -> Option> { 151 | chinese_remainder_theorem(facelets.iter().map(|&facelet| { 152 | let maps_to = permutation.mapping()[facelet]; 153 | 154 | let chromatic_order = generator.chromatic_orders_by_facelets()[facelet]; 155 | 156 | if maps_to == facelet { 157 | return Some((Int::zero(), chromatic_order)); 158 | } 159 | 160 | let mut i = Int::::one(); 161 | let mut maps_to_found_at = None; 162 | let mut facelet_at = generator.permutation().mapping()[facelet]; 163 | 164 | while facelet_at != facelet { 165 | if facelet_at == maps_to { 166 | maps_to_found_at = Some(i); 167 | break; 168 | } 169 | 170 | facelet_at = generator.permutation().mapping()[facelet_at]; 171 | i += Int::::one(); 172 | } 173 | 174 | maps_to_found_at.map(|found_at| (found_at % chromatic_order, chromatic_order)) 175 | })) 176 | } 177 | 178 | #[cfg(test)] 179 | mod tests { 180 | use std::sync::Arc; 181 | 182 | use internment::ArcIntern; 183 | 184 | use crate::{ 185 | Int, U, 186 | architectures::{Algorithm, PuzzleDefinition}, 187 | discrete_math::{ 188 | decode, extended_euclid, gcd, lcm, 189 | length_of_substring_that_this_string_is_n_repeated_copies_of, 190 | }, 191 | }; 192 | 193 | use super::chinese_remainder_theorem; 194 | 195 | #[test] 196 | fn lcm_and_gcd() { 197 | let lcm_int = |a: u64, b: u64| lcm(Int::from(a), Int::from(b)).to_u64(); 198 | let gcd_int = |a: u64, b: u64| gcd(Int::from(a), Int::from(b)).to_u64(); 199 | let extended_euclid_int = |a: u64, b: u64| { 200 | let ((x, y), z) = extended_euclid(Int::from(a), Int::from(b)); 201 | assert_eq!(Int::::from(a) * x + Int::::from(b) * y, z); 202 | z.to_u64() 203 | }; 204 | 205 | assert_eq!(gcd_int(3, 5), 1); 206 | assert_eq!(gcd_int(3, 6), 3); 207 | assert_eq!(gcd_int(4, 6), 2); 208 | 209 | assert_eq!(extended_euclid_int(3, 5), 1); 210 | assert_eq!(extended_euclid_int(3, 6), 3); 211 | assert_eq!(extended_euclid_int(4, 6), 2); 212 | 213 | assert_eq!(lcm_int(3, 5), 15); 214 | assert_eq!(lcm_int(3, 6), 6); 215 | assert_eq!(lcm_int(4, 6), 12); 216 | } 217 | 218 | fn crt_int(v: impl IntoIterator) -> Option { 219 | chinese_remainder_theorem( 220 | v.into_iter() 221 | .map(|(a, b)| Some((Int::from(a), Int::from(b)))), 222 | ) 223 | .map(|v| v.to_u64()) 224 | } 225 | 226 | #[test] 227 | fn crt() { 228 | assert_eq!(crt_int([(2, 3), (1, 2)]), Some(5)); 229 | assert_eq!(crt_int([(3, 4), (1, 2)]), Some(3)); 230 | assert_eq!(crt_int([(3, 4), (1, 2), (3, 5), (4, 7)]), Some(123)); 231 | assert_eq!(crt_int([(2, 4), (1, 2)]), None); 232 | } 233 | 234 | #[test] 235 | fn length_of_substring_whatever() { 236 | assert_eq!( 237 | length_of_substring_that_this_string_is_n_repeated_copies_of( 238 | ["a", "a", "a", "a"].into_iter() 239 | ), 240 | 1 241 | ); 242 | 243 | assert_eq!( 244 | length_of_substring_that_this_string_is_n_repeated_copies_of( 245 | ["a", "b", "a", "b"].into_iter() 246 | ), 247 | 2 248 | ); 249 | 250 | assert_eq!( 251 | length_of_substring_that_this_string_is_n_repeated_copies_of( 252 | ["a", "b", "a", "b", "a"].into_iter() 253 | ), 254 | 5 255 | ); 256 | 257 | assert_eq!( 258 | length_of_substring_that_this_string_is_n_repeated_copies_of( 259 | ["a", "b", "c", "d", "e"].into_iter() 260 | ), 261 | 5 262 | ); 263 | } 264 | 265 | #[test] 266 | fn test_decode() { 267 | let cube_def = Arc::new( 268 | PuzzleDefinition::parse(include_str!("../../../qter_core/puzzles/3x3.txt")).unwrap(), 269 | ); 270 | 271 | let mut cube = cube_def.perm_group.identity(); 272 | 273 | let permutation = Algorithm::new_from_move_seq( 274 | Arc::clone(&cube_def.perm_group), 275 | vec![ArcIntern::from("U")], 276 | ) 277 | .unwrap(); 278 | 279 | assert_eq!(decode(&cube, &[8], &permutation).unwrap(), Int::::zero()); 280 | 281 | cube.compose_into(permutation.permutation()); 282 | assert_eq!(decode(&cube, &[8], &permutation).unwrap(), Int::::one()); 283 | 284 | cube.compose_into(permutation.permutation()); 285 | assert_eq!(decode(&cube, &[8], &permutation).unwrap(), Int::from(2)); 286 | 287 | cube.compose_into(permutation.permutation()); 288 | assert_eq!(decode(&cube, &[8], &permutation).unwrap(), Int::from(3)); 289 | 290 | cube.compose_into(permutation.permutation()); 291 | assert_eq!(decode(&cube, &[8], &permutation).unwrap(), Int::from(0)); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/qter_core/src/math/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod discrete_math; 2 | mod numbers; 3 | pub mod schreier_sims; 4 | pub mod union_find; 5 | 6 | pub use numbers::*; 7 | -------------------------------------------------------------------------------- /src/qter_core/src/math/schreier_sims.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, option::Option, sync::Arc}; 2 | 3 | use itertools::Itertools; 4 | 5 | use crate::architectures::{Permutation, PermutationGroup}; 6 | 7 | use super::{I, Int, U}; 8 | 9 | pub struct StabilizerChain { 10 | stabilizers: Stabilizer, 11 | } 12 | 13 | impl StabilizerChain { 14 | /// Create a new stabilizer chain from the permutation group using the Schreier-Sims algorithm. 15 | #[must_use] 16 | pub fn new(group: &Arc) -> StabilizerChain { 17 | let mut stabilizers = 18 | Stabilizer::new(Arc::clone(group), &(0..group.facelet_count()).collect_vec()); 19 | 20 | for (_, perm) in group.generators() { 21 | stabilizers.extend(perm.to_owned()); 22 | } 23 | 24 | StabilizerChain { stabilizers } 25 | } 26 | 27 | /// Determine if a permutation is a member of the group 28 | #[must_use] 29 | pub fn is_member(&self, permutation: Permutation) -> bool { 30 | self.stabilizers.is_member(permutation) 31 | } 32 | 33 | /// Calculate the cardinality of the group 34 | #[must_use] 35 | pub fn cardinality(&self) -> Int { 36 | self.stabilizers.cardinality() 37 | } 38 | } 39 | 40 | #[derive(Debug)] 41 | struct Stabilizer { 42 | group: Arc, 43 | next: Option>, 44 | stabilizes: usize, 45 | generating_set: Vec, 46 | coset_reps: Box<[Option]>, 47 | } 48 | 49 | impl Stabilizer { 50 | fn new(group: Arc, chain: &[usize]) -> Stabilizer { 51 | let (head, tail) = chain.split_first().unwrap(); 52 | 53 | let mut coset_reps = Box::<[_]>::from(vec![None; group.facelet_count()]); 54 | coset_reps[*head] = Some(group.identity()); 55 | 56 | Stabilizer { 57 | stabilizes: *head, 58 | next: (!tail.is_empty()).then(|| Box::new(Stabilizer::new(Arc::clone(&group), tail))), 59 | coset_reps, 60 | generating_set: Vec::new(), 61 | group, 62 | } 63 | } 64 | 65 | fn cardinality(&self) -> Int { 66 | let mut cardinality = Int::from(self.coset_reps.iter().filter(|v| v.is_some()).count()); 67 | if let Some(next) = &self.next { 68 | cardinality *= next.cardinality(); 69 | } 70 | cardinality 71 | } 72 | 73 | #[must_use] 74 | fn is_member(&self, mut permutation: Permutation) -> bool { 75 | // println!("{} — {}", self.stabilizes, permutation); 76 | loop { 77 | let rep = permutation.mapping()[self.stabilizes]; 78 | 79 | if rep == self.stabilizes { 80 | break; 81 | } 82 | 83 | let Some(other_perm) = &self.coset_reps[rep] else { 84 | return false; 85 | }; 86 | 87 | permutation.compose_into(other_perm); 88 | } 89 | 90 | match &self.next { 91 | Some(next) => next.is_member(permutation), 92 | None => true, 93 | } 94 | } 95 | 96 | fn inverse_rep_to(&self, mut rep: usize, alg: &mut Permutation) -> Result<(), ()> { 97 | while rep != self.stabilizes { 98 | let Some(other_alg) = &self.coset_reps[rep] else { 99 | return Err(()); 100 | }; 101 | 102 | alg.compose_into(other_alg); 103 | rep = other_alg.mapping()[rep]; 104 | } 105 | 106 | Ok(()) 107 | } 108 | 109 | fn extend(&mut self, generator: Permutation) { 110 | if self.is_member(generator.clone()) { 111 | // TODO: Check if the generator is shorter than the ones we already have 112 | return; 113 | } 114 | // println!("{} {generator:?}", self.stabilizes); 115 | 116 | self.generating_set.push(generator); 117 | let generator = self.generating_set.last().unwrap(); 118 | 119 | let mapping = generator.mapping().to_owned(); 120 | let mut inv = generator.clone(); 121 | inv.exponentiate(-Int::::one()); 122 | 123 | // TODO: Some kind of SSSP thing to make these coset reps as short as possible 124 | let mut newly_in_orbit = VecDeque::new(); 125 | 126 | #[expect(clippy::needless_range_loop)] // false positive 127 | for i in 0..self.coset_reps.len() { 128 | if self.coset_reps[i].is_some() && self.coset_reps[mapping[i]].is_none() { 129 | self.coset_reps[mapping[i]] = Some(inv.clone()); 130 | newly_in_orbit.push_back(mapping[i]); 131 | } 132 | } 133 | 134 | while let Some(spot) = newly_in_orbit.pop_front() { 135 | for perm in &self.generating_set { 136 | let goes_to = perm.mapping()[spot]; 137 | if self.coset_reps[goes_to].is_none() { 138 | let mut inv_alg = perm.clone(); 139 | inv_alg.exponentiate(-Int::::one()); 140 | self.coset_reps[goes_to] = Some(inv_alg); 141 | newly_in_orbit.push_back(goes_to); 142 | } 143 | } 144 | } 145 | 146 | if self.next.is_none() { 147 | return; 148 | } 149 | 150 | for i in 0..self.coset_reps.len() { 151 | let mut rep = self.group.identity(); 152 | let Ok(()) = self.inverse_rep_to(i, &mut rep) else { 153 | continue; 154 | }; 155 | 156 | rep.exponentiate(-Int::::one()); 157 | 158 | for generator in &self.generating_set { 159 | let mut new_generator = rep.clone(); 160 | new_generator.compose_into(generator); 161 | self.inverse_rep_to(new_generator.mapping()[self.stabilizes], &mut new_generator) 162 | .unwrap(); 163 | self.next.as_mut().unwrap().extend(new_generator); 164 | } 165 | } 166 | } 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use std::{collections::HashMap, sync::Arc}; 172 | 173 | use internment::ArcIntern; 174 | 175 | use crate::{ 176 | Int, U, 177 | architectures::{Algorithm, Permutation, PermutationGroup, PuzzleDefinition}, 178 | }; 179 | 180 | use super::StabilizerChain; 181 | 182 | #[test] 183 | fn simple() { 184 | let mut perms = HashMap::new(); 185 | perms.insert( 186 | ArcIntern::from("A"), 187 | Permutation::from_cycles(vec![vec![0, 1, 2]]), 188 | ); 189 | perms.insert( 190 | ArcIntern::from("B"), 191 | Permutation::from_cycles(vec![vec![0, 2, 1]]), 192 | ); 193 | 194 | let puzzle = Arc::new(PermutationGroup::new( 195 | vec![ 196 | ArcIntern::from("a"), 197 | ArcIntern::from("b"), 198 | ArcIntern::from("c"), 199 | ], 200 | perms, 201 | )); 202 | 203 | let method = StabilizerChain::new(&puzzle); 204 | assert_eq!(method.cardinality(), Int::::from(3_u32)); 205 | assert!(!method.is_member(Permutation::from_cycles(vec![vec![0, 1]]))); 206 | assert!(method.is_member(Permutation::from_cycles(vec![vec![0, 1, 2]]))); 207 | } 208 | 209 | #[test] 210 | fn three_by_three() { 211 | let cube_def = PuzzleDefinition::parse(include_str!("../../puzzles/3x3.txt")) 212 | .unwrap() 213 | .perm_group; 214 | 215 | let method = StabilizerChain::new(&cube_def); 216 | 217 | assert_eq!( 218 | method.cardinality(), 219 | "43252003274489856000".parse::>().unwrap() 220 | ); 221 | 222 | // Corner twist 223 | assert!(!method.is_member(Permutation::from_cycles(vec![vec![10, 16, 5]]))); 224 | 225 | // U alg 226 | assert!( 227 | method.is_member( 228 | Algorithm::new_from_move_seq(Arc::clone(&cube_def), vec![ArcIntern::from("U")]) 229 | .unwrap() 230 | .permutation() 231 | .clone() 232 | ) 233 | ); 234 | 235 | // Sexy move 236 | assert!( 237 | method.is_member( 238 | Algorithm::new_from_move_seq( 239 | Arc::clone(&cube_def), 240 | vec![ 241 | ArcIntern::from("U"), 242 | ArcIntern::from("R"), 243 | ArcIntern::from("U'"), 244 | ArcIntern::from("R'"), 245 | ] 246 | ) 247 | .unwrap() 248 | .permutation() 249 | .clone() 250 | ) 251 | ); 252 | 253 | // Two corner twists to make the orientation sum work 254 | assert!(method.is_member(Permutation::from_cycles(vec![ 255 | vec![10, 16, 5], 256 | vec![18, 7, 24] 257 | ]))); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/qter_core/src/math/union_find.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Cell, Ref, RefCell}, 3 | mem, 4 | }; 5 | 6 | /// Information about each disjoint set and path as well as how to merge them together 7 | /// 8 | /// The type that implements this trait is the type representing information for each set 9 | /// 10 | /// The set info is metadata associated with each disjoint set in union-find. 11 | /// 12 | /// The path info is info associated with each element of the union find, describing its path to the root. 13 | pub trait SetInfo { 14 | /// The type representing information for each path 15 | type PathInfo; 16 | 17 | /// Whether to allow weighted quick-union (for better performance) or to force unions to happen in the order specified by the arguments to `union` 18 | const ALLOW_WEIGHTED: bool = false; 19 | 20 | /// Merge the info for two sets, used on the `union` call. Return the path info for the new child. 21 | fn merge(&mut self, new_child: Self); 22 | 23 | /// Join a path with the path of its parent. This function must be associative. 24 | fn join_paths(path: &mut Self::PathInfo, path_of_parent: &Self::PathInfo); 25 | } 26 | 27 | impl SetInfo for () { 28 | type PathInfo = (); 29 | 30 | const ALLOW_WEIGHTED: bool = true; 31 | 32 | fn merge(&mut self, _new_child: Self) {} 33 | 34 | fn join_paths(_path: &mut Self::PathInfo, _path_of_old_root: &Self::PathInfo) {} 35 | } 36 | 37 | enum UnionFindEntry { 38 | RootOfSet { 39 | // For weighted union-find 40 | weight: usize, 41 | set_meta: S, 42 | }, 43 | OwnedBy { 44 | owned_by: Cell, 45 | }, 46 | } 47 | 48 | /// A data structure allowing you track disjoint sets of numbers. In qter, this means orbits in a permutation group but you can use it for anything. 49 | /// 50 | /// This structure also keeps track of metadata for each set and element. If you do not need this, use `()` for the `S` parameter. 51 | pub struct UnionFind { 52 | sets: Box<[(UnionFindEntry, RefCell>)]>, 53 | } 54 | 55 | /// Information about an element, returned by the `find` operation 56 | pub struct FindResult<'a, S: SetInfo> { 57 | root_idx: usize, 58 | set_size: usize, 59 | set_meta: &'a S, 60 | path_meta: Ref<'a, Option>, 61 | } 62 | 63 | impl FindResult<'_, S> { 64 | /// Returns the index of the element representing the root of the set 65 | #[must_use] 66 | pub fn root_idx(&self) -> usize { 67 | self.root_idx 68 | } 69 | 70 | /// The total size of the set 71 | #[must_use] 72 | pub fn set_size(&self) -> usize { 73 | self.set_size 74 | } 75 | 76 | /// Metadata associated with the set the element is a member of 77 | #[must_use] 78 | pub fn set_meta(&self) -> &S { 79 | self.set_meta 80 | } 81 | 82 | /// Metadata associated with the path from this element to the root 83 | #[must_use] 84 | pub fn path_meta(&self) -> Option<&S::PathInfo> { 85 | self.path_meta.as_ref() 86 | } 87 | } 88 | 89 | impl UnionFind { 90 | pub fn new(item_count: usize) -> Self { 91 | let mut sets = Vec::with_capacity(item_count); 92 | 93 | for _ in 0..item_count { 94 | sets.push(( 95 | UnionFindEntry::RootOfSet { 96 | weight: 1, 97 | set_meta: S::default(), 98 | }, 99 | RefCell::new(None), 100 | )); 101 | } 102 | 103 | UnionFind { 104 | sets: Box::from(sets), 105 | } 106 | } 107 | } 108 | 109 | impl UnionFind { 110 | /// Create a new `UnionFind` with the given number of elements 111 | pub fn new_with_initial_set_info(set_infos: Vec) -> Self { 112 | let mut sets = Vec::with_capacity(set_infos.len()); 113 | 114 | for info in set_infos { 115 | sets.push(( 116 | UnionFindEntry::RootOfSet { 117 | weight: 1, 118 | set_meta: info, 119 | }, 120 | RefCell::new(None), 121 | )); 122 | } 123 | 124 | UnionFind { 125 | sets: Box::from(sets), 126 | } 127 | } 128 | 129 | /// Find an element in the `UnionFind` and return metadata about it. 130 | /// 131 | /// Panics if the item is outside the range of numbers in the union-find. 132 | #[must_use] 133 | #[expect(clippy::missing_panics_doc)] 134 | pub fn find(&self, item: usize) -> FindResult { 135 | let (entry, path_meta) = &self.sets[item]; 136 | 137 | match entry { 138 | UnionFindEntry::RootOfSet { weight, set_meta } => FindResult { 139 | root_idx: item, 140 | set_size: *weight, 141 | set_meta, 142 | path_meta: path_meta.borrow(), 143 | }, 144 | UnionFindEntry::OwnedBy { owned_by } => { 145 | let mut ret = self.find(owned_by.get()); 146 | 147 | if let Some(root_meta) = ret.path_meta() { 148 | owned_by.set(ret.root_idx); 149 | 150 | // This borrow_mut is valid despite a `Ref` being returned because once this function returns, the node is guaranteed to be a child of a root and compression will not happen again until `union` is called. This function returns an `&` reference to the union-find and `union` takes and `&mut` reference so `union` will not be called until the `Ref` is dropped. 151 | let mut path_meta_mut = path_meta.borrow_mut(); 152 | // This element has a parent, therefore the `path_meta` cannot be null 153 | S::join_paths((*path_meta_mut).as_mut().unwrap(), root_meta); 154 | drop(path_meta_mut); 155 | ret.path_meta = path_meta.borrow(); 156 | } 157 | 158 | ret 159 | } 160 | } 161 | } 162 | 163 | /// Union the sets that the two representatives given belong to, with `child` becoming a child of `parent`. 164 | /// 165 | /// Panics if either `parent` or `child` are outside of the range of elements in the union-find. 166 | /// 167 | /// If `S::ALLOW_WEIGHTED` is `true`, then this will implement weighted quick union and `parent` and `child` may be swapped for performance. 168 | pub fn union(&mut self, parent: usize, child: usize, path_info: S::PathInfo) { 169 | let mut a_result = self.find(parent); 170 | let mut b_result = self.find(child); 171 | 172 | if a_result.root_idx == b_result.root_idx { 173 | return; 174 | } 175 | 176 | if S::ALLOW_WEIGHTED && a_result.set_size < b_result.set_size { 177 | mem::swap(&mut a_result, &mut b_result); 178 | } 179 | 180 | let a_idx = a_result.root_idx; 181 | let b_size = b_result.set_size; 182 | let b_idx = b_result.root_idx; 183 | 184 | drop(a_result); 185 | drop(b_result); 186 | 187 | self.sets[b_idx].1.replace(Some(path_info)); 188 | 189 | let old_b_data = mem::replace( 190 | &mut self.sets[b_idx].0, 191 | UnionFindEntry::OwnedBy { 192 | owned_by: Cell::new(a_idx), 193 | }, 194 | ); 195 | 196 | let other_set_meta = match old_b_data { 197 | UnionFindEntry::RootOfSet { 198 | weight: _, 199 | set_meta, 200 | } => set_meta, 201 | UnionFindEntry::OwnedBy { owned_by: _ } => unreachable!(), 202 | }; 203 | 204 | match &mut self.sets[a_idx].0 { 205 | UnionFindEntry::RootOfSet { weight, set_meta } => { 206 | *weight += b_size; 207 | 208 | set_meta.merge(other_set_meta); 209 | } 210 | UnionFindEntry::OwnedBy { owned_by: _ } => unreachable!(), 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/qter_core/src/puzzle.pest: -------------------------------------------------------------------------------- 1 | WHITESPACE = _{ " " } 2 | 3 | description = { lb? ~ colors ~ generators ~ derived ~ presets ~ EOI } 4 | 5 | lb = _{ "\n"+ } 6 | ident = @{ ( !(WHITESPACE | "\n" | "~" | "/") ~ ANY )+ } 7 | number = @{ ASCII_DIGIT+ } 8 | 9 | colors = { "COLORS" ~ lb ~ color+ } 10 | color = { ident ~ "->" ~ number ~ ("," ~ number)* ~ lb } 11 | 12 | generators = { "GENERATORS" ~ lb ~ generator+ } 13 | 14 | generator = { ident ~ "=" ~ cycle+ ~ lb } 15 | cycle = { "(" ~ number ~ ("," ~ number)* ~ ")" } 16 | 17 | derived = { "DERIVED" ~ lb ~ derivation* } 18 | derivation = { ident ~ "=" ~ ident+ ~ lb } 19 | 20 | presets = { "PRESETS" ~ lb ~ preset* } 21 | preset = { orders ~ algos ~ ("~" ~ number)? ~ lb } 22 | orders = { "(" ~ number ~ ("," ~ number)* ~ ")" } 23 | algos = { algo ~ ("/" ~ algo)* } 24 | algo = { (!"/" ~ ident)+ } 25 | 26 | -------------------------------------------------------------------------------- /src/qter_core/src/puzzle_parser.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap, sync::Arc}; 2 | 3 | use internment::ArcIntern; 4 | use itertools::Itertools; 5 | use pest::{Parser, error::Error}; 6 | use pest_derive::Parser; 7 | 8 | use crate::{ 9 | Int, U, 10 | architectures::{ 11 | Architecture, OPTIMIZED_TABLES, Permutation, PermutationGroup, PuzzleDefinition, 12 | }, 13 | }; 14 | 15 | #[derive(Parser)] 16 | #[grammar = "./puzzle.pest"] 17 | struct SpecParser; 18 | 19 | pub fn parse(spec: &str) -> Result>> { 20 | let mut parsed = SpecParser::parse(Rule::description, spec)? 21 | .next() 22 | .unwrap() 23 | .into_inner(); 24 | 25 | let colors_pair = parsed.next().unwrap(); 26 | 27 | let mut colors_map = HashMap::>::new(); 28 | 29 | let mut min_facelet = usize::MAX; 30 | let mut max_facelet = usize::MIN; 31 | 32 | let colors_span = colors_pair.as_span(); 33 | 34 | for pair in colors_pair.into_inner() { 35 | let mut pairs = pair.into_inner(); 36 | let color = pairs.next().unwrap().as_str(); 37 | 38 | let mut facelets = vec![]; 39 | 40 | for pair in pairs { 41 | let facelet = pair.as_str().parse::().unwrap(); 42 | 43 | if min_facelet > facelet { 44 | min_facelet = facelet; 45 | } 46 | 47 | if max_facelet < facelet { 48 | max_facelet = facelet; 49 | } 50 | 51 | facelets.push(facelet); 52 | } 53 | 54 | colors_map.insert(color.to_owned(), facelets); 55 | } 56 | 57 | let empty_string = ArcIntern::::from(""); 58 | let mut colors = vec![empty_string; max_facelet - min_facelet + 1]; 59 | 60 | // Make facelets zero based 61 | for (color, facelets) in colors_map { 62 | let color = ArcIntern::from(color); 63 | 64 | for facelet in facelets { 65 | colors[facelet - min_facelet] = ArcIntern::clone(&color); 66 | } 67 | } 68 | 69 | for color in &colors { 70 | if color.is_empty() { 71 | return Err(Box::new(Error::new_from_span( 72 | pest::error::ErrorVariant::CustomError { 73 | message: "Didn't define the color for every facelet".to_owned(), 74 | }, 75 | colors_span, 76 | ))); 77 | } 78 | } 79 | 80 | let generators_pair = parsed.next().unwrap().into_inner(); 81 | 82 | let mut generators = HashMap::new(); 83 | 84 | for pair in generators_pair { 85 | let mut pairs = pair.into_inner(); 86 | 87 | let name = pairs.next().unwrap().as_str(); 88 | 89 | let mut cycles = vec![]; 90 | 91 | for cycle_pair in pairs { 92 | let mut cycle = vec![]; 93 | 94 | for value in cycle_pair.into_inner() { 95 | let facelet_span = value.as_span(); 96 | let facelet = value.as_str().parse::().unwrap(); 97 | 98 | if min_facelet > facelet || max_facelet < facelet { 99 | return Err(Box::new(Error::new_from_span( 100 | pest::error::ErrorVariant::CustomError { 101 | message: "Facelet is out of range".to_owned(), 102 | }, 103 | facelet_span, 104 | ))); 105 | } 106 | 107 | cycle.push(facelet - min_facelet); 108 | } 109 | 110 | cycles.push(cycle); 111 | } 112 | 113 | let mut permutation = Permutation::from_cycles(cycles); 114 | 115 | permutation.facelet_count = max_facelet - min_facelet + 1; 116 | 117 | generators.insert(ArcIntern::from(name), permutation); 118 | } 119 | 120 | let derived_pair = parsed.next().unwrap().into_inner(); 121 | 122 | for pair in derived_pair { 123 | let mut pairs = pair.into_inner(); 124 | 125 | let name = pairs.next().unwrap().as_str(); 126 | 127 | let permutation_name = pairs.next().unwrap(); 128 | let mut permutation = match generators.get(&ArcIntern::from(permutation_name.as_str())) { 129 | Some(v) => v.to_owned(), 130 | None => { 131 | return Err(Box::new(Error::new_from_span( 132 | pest::error::ErrorVariant::CustomError { 133 | message: format!( 134 | "The permutation {} doesn't exist", 135 | permutation_name.as_str(), 136 | ), 137 | }, 138 | permutation_name.as_span(), 139 | ))); 140 | } 141 | }; 142 | 143 | for pair in pairs { 144 | let Some(next_permutation) = generators.get(&ArcIntern::from(pair.as_str())) else { 145 | return Err(Box::new(Error::new_from_span( 146 | pest::error::ErrorVariant::CustomError { 147 | message: format!( 148 | "The permutation {} doesn't exist", 149 | permutation_name.as_str(), 150 | ), 151 | }, 152 | permutation_name.as_span(), 153 | ))); 154 | }; 155 | 156 | permutation.compose_into(next_permutation); 157 | } 158 | 159 | generators.insert(ArcIntern::from(name), permutation); 160 | } 161 | 162 | let group = Arc::new(PermutationGroup::new(colors, generators)); 163 | 164 | let presets_pairs = parsed.next().unwrap().into_inner(); 165 | 166 | let mut presets = vec![]; 167 | 168 | for preset_pair in presets_pairs { 169 | let algorithm_span = preset_pair.as_span(); 170 | let mut preset_pairs = preset_pair.into_inner(); 171 | 172 | let orders_pair = preset_pairs.next().unwrap(); 173 | let mut orders = vec![]; 174 | 175 | for order in orders_pair.into_inner() { 176 | orders.push(order.as_str().parse::>().unwrap()); 177 | } 178 | 179 | let algs_pair = preset_pairs.next().unwrap(); 180 | let algs_pairs = algs_pair.into_inner(); 181 | 182 | let mut algorithms = vec![]; 183 | 184 | for algorithm_pair in algs_pairs { 185 | let mut moves = vec![]; 186 | 187 | for action in algorithm_pair.into_inner() { 188 | moves.push(ArcIntern::from(action.as_str())); 189 | } 190 | 191 | algorithms.push(moves); 192 | } 193 | 194 | let mut architecture = match Architecture::new(Arc::clone(&group), &algorithms) { 195 | Ok(v) => v, 196 | Err(e) => { 197 | return Err(Box::new(Error::new_from_span( 198 | pest::error::ErrorVariant::CustomError { 199 | message: format!("Generator doesn't exist: {e}"), 200 | }, 201 | algorithm_span, 202 | ))); 203 | } 204 | }; 205 | 206 | if let Some(pregenerated_idx) = preset_pairs.next() { 207 | architecture.set_optimized_table(Cow::Borrowed( 208 | OPTIMIZED_TABLES[pregenerated_idx.as_str().parse::().unwrap()], 209 | )); 210 | } 211 | 212 | for (register, order) in architecture.registers().iter().zip(orders.into_iter()) { 213 | if register.order() != order { 214 | return Err(Box::new(Error::new_from_span( 215 | pest::error::ErrorVariant::CustomError { 216 | message: format!( 217 | "The algorithm {} has an incorrect order. Expected order {order} but found order {}.", 218 | register.algorithm().move_seq_iter().join(" "), 219 | register.order() 220 | ), 221 | }, 222 | algorithm_span, 223 | ))); 224 | } 225 | } 226 | 227 | presets.push(Arc::new(architecture)); 228 | } 229 | 230 | Ok(PuzzleDefinition { 231 | perm_group: group, 232 | presets, 233 | }) 234 | } 235 | -------------------------------------------------------------------------------- /src/qter_core/src/runtime.rs: -------------------------------------------------------------------------------- 1 | use crate::architectures::{Algorithm, PermutationGroup}; 2 | use crate::{Int, U, WithSpan}; 3 | use std::sync::Arc; 4 | 5 | /// The facelets needed for `solved-goto` 6 | #[derive(Debug, Clone)] 7 | pub enum Facelets { 8 | Theoretical, 9 | Puzzle { facelets: Vec }, 10 | } 11 | 12 | /// The generator of a register along with the facelets needed to decode it 13 | #[derive(Debug, Clone)] 14 | pub enum RegisterGenerator { 15 | Theoretical, 16 | Puzzle { 17 | generator: Box, 18 | solved_goto_facelets: Vec, 19 | }, 20 | } 21 | 22 | /// A qter instruction 23 | #[derive(Debug)] 24 | pub enum Instruction { 25 | Goto { 26 | instruction_idx: usize, 27 | }, 28 | SolvedGoto { 29 | instruction_idx: usize, 30 | puzzle_idx: usize, 31 | facelets: Facelets, 32 | }, 33 | Input { 34 | message: String, 35 | puzzle_idx: usize, 36 | register: RegisterGenerator, 37 | }, 38 | Halt { 39 | message: String, 40 | maybe_puzzle_idx_and_register: Option<(usize, RegisterGenerator)>, 41 | }, 42 | Print { 43 | message: String, 44 | maybe_puzzle_idx_and_register: Option<(usize, RegisterGenerator)>, 45 | }, 46 | /// Add to a theoretical register; has no representation in .Q 47 | AddTheoretical { 48 | puzzle_idx: usize, 49 | amount: Int, 50 | }, 51 | Algorithm { 52 | puzzle_idx: usize, 53 | algorithm: Algorithm, 54 | }, 55 | } 56 | 57 | /// A qter program 58 | #[derive(Debug)] 59 | pub struct Program { 60 | /// A list of theoretical registers along with their orders 61 | pub theoretical: Vec>>, 62 | /// A list of puzzles to be used for registers 63 | pub puzzles: Vec>>, 64 | /// The program itself 65 | pub instructions: Vec>, 66 | } 67 | -------------------------------------------------------------------------------- /src/qter_core/src/shared_facelet_detection.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, sync::Arc}; 2 | 3 | use internment::ArcIntern; 4 | use itertools::Itertools; 5 | 6 | use crate::{ 7 | Int, 8 | architectures::{ 9 | Algorithm, CycleGenerator, CycleGeneratorSubcycle, Permutation, PermutationGroup, 10 | }, 11 | discrete_math::length_of_substring_that_this_string_is_n_repeated_copies_of, 12 | union_find::{SetInfo, UnionFind}, 13 | }; 14 | 15 | struct FaceletSources(HashSet); 16 | 17 | impl SetInfo for FaceletSources { 18 | type PathInfo = (); 19 | 20 | const ALLOW_WEIGHTED: bool = true; 21 | 22 | fn merge(&mut self, new_child: Self) -> Self::PathInfo { 23 | self.0.extend(new_child.0); 24 | } 25 | 26 | fn join_paths(_path: &mut Self::PathInfo, _path_of_parent: &Self::PathInfo) {} 27 | } 28 | 29 | /// Calculate the orbits of all of the facelets along with which algorithms contribute to the orbit 30 | fn find_orbits(facelet_count: usize, permutations: &[Permutation]) -> UnionFind { 31 | // Initialize the union-find 32 | let mut sets = vec![]; 33 | 34 | for facelet in 0..facelet_count { 35 | // For each facelet, find which algorithms move it and add them to the set 36 | let mut contains_facelets_from = HashSet::new(); 37 | 38 | for (i, permutation) in permutations.iter().enumerate() { 39 | if permutation.mapping()[facelet] != facelet { 40 | contains_facelets_from.insert(i); 41 | } 42 | } 43 | 44 | sets.push(FaceletSources(contains_facelets_from)); 45 | } 46 | 47 | let mut union_find = UnionFind::new_with_initial_set_info(sets); 48 | 49 | // Union all facelets that share the same orbit 50 | for permutation in permutations { 51 | for facelet in 0..facelet_count { 52 | let goes_to = permutation.mapping()[facelet]; 53 | 54 | // They have the same orbit if one is mapped to the other 55 | union_find.union(facelet, goes_to, ()); 56 | } 57 | } 58 | 59 | union_find 60 | } 61 | 62 | /// Convert the algorithms into a list of cycle generators and a list of shared facelets 63 | /// 64 | /// # Errors 65 | /// 66 | /// If either of the algorithms have an invalid generator, the function will compose all of the generators before it and return the name of the generator that doesn't exist as an error 67 | pub fn algorithms_to_cycle_generators( 68 | group: &Arc, 69 | algorithms: &[Vec>], 70 | ) -> Result<(Vec, Vec), ArcIntern> { 71 | // Calculate the permutations generated by each algorithm 72 | let mut permutations = vec![]; 73 | 74 | for algorithm in algorithms { 75 | let mut permutation = group.identity(); 76 | group.compose_generators_into(&mut permutation, algorithm.iter())?; 77 | permutations.push(permutation); 78 | } 79 | 80 | // Find the orbits of all of the facelets in the subgroup generated by `permutations` 81 | let orbits = find_orbits(group.facelet_count(), &permutations); 82 | 83 | let mut shared_facelets = vec![]; 84 | 85 | Ok(( 86 | permutations 87 | .into_iter() 88 | .zip(algorithms.iter()) 89 | .map(|(permutation, algorithm)| { 90 | // Dump all unshared facelets out of the union-find into a list and all shared facelets into the shared_facelets list 91 | let mut unshared_cycles = vec![]; 92 | 93 | for cycle in permutation.cycles() { 94 | if orbits.find(cycle[0]).set_meta().0.len() > 1 { 95 | shared_facelets.extend_from_slice(cycle); 96 | continue; 97 | } 98 | 99 | let chromatic_order = 100 | length_of_substring_that_this_string_is_n_repeated_copies_of( 101 | cycle.iter().map(|&idx| &*group.facelet_colors()[idx]), 102 | ); 103 | 104 | unshared_cycles.push(CycleGeneratorSubcycle { 105 | facelet_cycle: cycle.to_owned(), 106 | chromatic_order: Int::from(chromatic_order), 107 | }); 108 | } 109 | 110 | CycleGenerator::new( 111 | Algorithm::new_from_move_seq(Arc::clone(group), algorithm.to_owned()).unwrap(), 112 | unshared_cycles, 113 | ) 114 | }) 115 | .collect(), 116 | shared_facelets.into_iter().unique().collect_vec(), 117 | )) 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use crate::{ 123 | Int, 124 | architectures::{CycleGeneratorSubcycle, PuzzleDefinition}, 125 | }; 126 | 127 | #[test] 128 | fn simple() { 129 | let PuzzleDefinition { 130 | perm_group: _, 131 | presets, 132 | } = PuzzleDefinition::parse( 133 | " 134 | COLORS 135 | 136 | A -> 1 137 | B -> 2 138 | C -> 3 139 | D -> 4 140 | E -> 5 141 | F -> 6 142 | G -> 7 143 | H -> 8 144 | I -> 9 145 | J -> 10 146 | K -> 11, 12, 13 147 | 148 | GENERATORS 149 | 150 | A = (1, 2, 3) 151 | B = (4, 5, 6) 152 | C = (6, 7, 8) 153 | D = (9, 10) 154 | E = (11, 12, 13) 155 | 156 | A' = (3, 2, 1) 157 | B' = (6, 5, 4) 158 | C' = (8, 7, 6) 159 | E' = (13, 12, 11) 160 | 161 | DERIVED 162 | 163 | PRESETS 164 | 165 | (3, 2) A B / C D E 166 | ", 167 | ) 168 | .unwrap(); 169 | 170 | let preset = &presets[0]; 171 | 172 | for i in 3..=7 { 173 | assert!(preset.shared_facelets().contains(&i)); 174 | } 175 | 176 | assert_eq!(preset.registers()[0].order(), Int::from(3)); 177 | assert_eq!( 178 | preset.registers()[0].unshared_cycles(), 179 | vec![CycleGeneratorSubcycle { 180 | facelet_cycle: vec![0, 1, 2], 181 | chromatic_order: Int::from(3_usize), 182 | }] 183 | ); 184 | assert_eq!(preset.registers()[1].order(), Int::from(2)); 185 | assert_eq!( 186 | preset.registers()[1].unshared_cycles(), 187 | vec![ 188 | CycleGeneratorSubcycle { 189 | facelet_cycle: vec![8, 9], 190 | chromatic_order: Int::from(2_usize) 191 | }, 192 | CycleGeneratorSubcycle { 193 | facelet_cycle: vec![10, 11, 12], 194 | chromatic_order: Int::one(), 195 | } 196 | ] 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/qter_core/src/span.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut}, 3 | sync::OnceLock, 4 | }; 5 | 6 | use internment::ArcIntern; 7 | use pest::{Position, RuleType}; 8 | 9 | pub fn mk_error( 10 | message: impl Into, 11 | loc: impl AsPestLoc, 12 | ) -> Box> { 13 | let err = pest::error::ErrorVariant::CustomError { 14 | message: message.into(), 15 | }; 16 | 17 | Box::new(match loc.as_pest_loc() { 18 | SpanOrPos::Span(span) => pest::error::Error::new_from_span(err, span), 19 | SpanOrPos::Pos(pos) => pest::error::Error::new_from_pos(err, pos), 20 | }) 21 | } 22 | 23 | pub enum SpanOrPos<'a> { 24 | Span(pest::Span<'a>), 25 | Pos(pest::Position<'a>), 26 | } 27 | 28 | pub trait AsPestLoc { 29 | fn as_pest_loc(&self) -> SpanOrPos<'_>; 30 | } 31 | 32 | impl AsPestLoc for pest::Span<'_> { 33 | fn as_pest_loc(&self) -> SpanOrPos<'_> { 34 | SpanOrPos::Span(self.to_owned()) 35 | } 36 | } 37 | 38 | impl AsPestLoc for Span { 39 | fn as_pest_loc(&self) -> SpanOrPos<'_> { 40 | SpanOrPos::Span(self.pest()) 41 | } 42 | } 43 | 44 | impl AsPestLoc for Position<'_> { 45 | fn as_pest_loc(&self) -> SpanOrPos<'_> { 46 | SpanOrPos::Pos(self.to_owned()) 47 | } 48 | } 49 | 50 | impl AsPestLoc for &T { 51 | fn as_pest_loc(&self) -> SpanOrPos<'_> { 52 | (*self).as_pest_loc() 53 | } 54 | } 55 | 56 | /// A slice of the original source code; to be attached to pieces of data for error reporting 57 | #[derive(Clone)] 58 | pub struct Span { 59 | source: ArcIntern, 60 | start: usize, 61 | end: usize, 62 | line_and_col: OnceLock<(usize, usize)>, 63 | } 64 | 65 | impl Span { 66 | #[must_use] 67 | pub fn from_span(span: pest::Span) -> Span { 68 | Span::new(ArcIntern::from(span.get_input()), span.start(), span.end()) 69 | } 70 | 71 | /// Creates a new `Span` from the given source and start/end positions 72 | /// 73 | /// # Panics 74 | /// 75 | /// Panics if the start or end positions are out of bounds, or if the start is greater than the end 76 | #[must_use] 77 | pub fn new(source: ArcIntern, start: usize, end: usize) -> Span { 78 | assert!(start <= end); 79 | assert!(start < source.len()); 80 | assert!(end <= source.len()); 81 | 82 | Span { 83 | source, 84 | start, 85 | end, 86 | line_and_col: OnceLock::new(), 87 | } 88 | } 89 | 90 | pub fn slice(&self) -> &str { 91 | &self.source[self.start..self.end] 92 | } 93 | 94 | pub fn line_and_col(&self) -> (usize, usize) { 95 | *self.line_and_col.get_or_init(|| { 96 | let mut current_line = 1; 97 | let mut current_col = 1; 98 | 99 | for c in self.source.chars().take(self.start) { 100 | if c == '\n' { 101 | current_line += 1; 102 | current_col = 1; 103 | } else { 104 | current_col += 1; 105 | } 106 | } 107 | 108 | (current_line, current_col) 109 | }) 110 | } 111 | 112 | pub fn line(&self) -> usize { 113 | self.line_and_col().0 114 | } 115 | 116 | pub fn col(&self) -> usize { 117 | self.line_and_col().1 118 | } 119 | 120 | #[must_use] 121 | pub fn after(mut self) -> Span { 122 | self.start = self.end; 123 | self 124 | } 125 | 126 | pub fn source(&self) -> ArcIntern { 127 | ArcIntern::clone(&self.source) 128 | } 129 | 130 | /// Merges two spans into one, keeping the earliest start and latest end 131 | /// 132 | /// # Panics 133 | /// 134 | /// Panics if the two spans are from different sources 135 | #[must_use] 136 | pub fn merge(self, other: &Span) -> Span { 137 | assert_eq!(self.source, other.source); 138 | 139 | Span { 140 | source: self.source, 141 | start: self.start.min(other.start), 142 | end: self.end.max(other.end), 143 | line_and_col: OnceLock::new(), 144 | } 145 | } 146 | 147 | fn pest(&self) -> pest::Span<'_> { 148 | pest::Span::new(&self.source, self.start, self.end).unwrap() 149 | } 150 | } 151 | 152 | impl core::fmt::Debug for Span { 153 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 154 | f.write_str(self.slice()) 155 | } 156 | } 157 | 158 | impl From> for Span { 159 | fn from(value: pest::Span) -> Self { 160 | Span::from_span(value) 161 | } 162 | } 163 | 164 | /// A value attached to a `Span` 165 | #[derive(Clone)] 166 | pub struct WithSpan { 167 | pub value: T, 168 | span: Span, 169 | } 170 | 171 | impl core::fmt::Debug for WithSpan { 172 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 173 | core::fmt::Debug::fmt(&self.value, f) 174 | } 175 | } 176 | 177 | impl Deref for WithSpan { 178 | type Target = T; 179 | 180 | fn deref(&self) -> &Self::Target { 181 | &self.value 182 | } 183 | } 184 | 185 | impl DerefMut for WithSpan { 186 | fn deref_mut(&mut self) -> &mut Self::Target { 187 | &mut self.value 188 | } 189 | } 190 | 191 | impl WithSpan { 192 | pub fn new(value: T, span: Span) -> WithSpan { 193 | WithSpan { value, span } 194 | } 195 | 196 | pub fn into_inner(self) -> T { 197 | self.value 198 | } 199 | 200 | pub fn map(self, f: impl FnOnce(T) -> V) -> WithSpan { 201 | WithSpan { 202 | value: f(self.value), 203 | span: self.span, 204 | } 205 | } 206 | 207 | pub fn span(&self) -> &Span { 208 | &self.span 209 | } 210 | 211 | pub fn line(&self) -> usize { 212 | self.span().line() 213 | } 214 | } 215 | 216 | impl PartialEq for WithSpan { 217 | fn eq(&self, other: &Self) -> bool { 218 | self.value == other.value 219 | } 220 | } 221 | 222 | impl Eq for WithSpan {} 223 | 224 | impl core::hash::Hash for WithSpan { 225 | fn hash(&self, state: &mut H) { 226 | self.value.hash(state); 227 | } 228 | } 229 | --------------------------------------------------------------------------------