Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,20 @@ cross-project vectors in `test/cube/notation/orbit64Vectors.json` and
The Orbit64 V2 state codec for 2x2-15x15 cubes
([`Orbit64V2.res`](../src/cube/notation/Orbit64V2.res)) works on orbit
coordinates. [`Orbit64V2Facelets.res`](../src/cube/notation/Orbit64V2Facelets.res)
is its facelet bridge for the color model, both poses: slot tables, cube to
facelets and back, and token to facelets and back, with every failure an error
value. The picture model has no facelet letters and is an error there. Its
fixture `test/cube/notation/orbit64V2Facelets.json` (the slot tables of
`FORMAT-V2-SLOTS.md` and the facelet rows of `TestV2FaceletData.flix`) is written
by `julia scripts/orbit64V2FaceletFixture.jl <flix-orbit64 dir>` and
regenerates byte for byte.
is its facelet bridge, both poses: slot tables, cube to facelets and back, and
token to facelets and back, with every failure an error value. The color model
writes the letters `U R F D L B`. The picture model (3x3x3 and up; the 2x2x2
has no picture entry) writes one base64url symbol per facelet, the sticker's
index within its class, so a picture string is as long as a color string. Both
read in the validation order of `FORMAT-V2.md` and give the reference's reason.
A cube handed to the bridge is copied once, by index, before it is checked, and
a string is walked once, so hostile arrays and getters cost bounded work, a
huge string costs linear time but no memory beyond the symbols of one cube,
and nothing can change between the check and the write. The fixtures
`test/cube/notation/orbit64V2Facelets.json` (the slot tables of
`FORMAT-V2-SLOTS.md` and the color rows of `TestV2FaceletData.flix`) and
`test/cube/notation/orbit64V2FaceletPictures.json` (the picture rows of
`TestV2FaceletPictureData.flix`) are written by
`julia scripts/orbit64V2FaceletFixture.jl <flix-orbit64 dir>`, which checks the
reference rows for dropped, duplicated and unparsed entries and regenerates both
files byte for byte.
108 changes: 99 additions & 9 deletions scripts/orbit64V2FaceletFixture.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
#!/usr/bin/env julia
#
# Writes test/cube/notation/orbit64V2Facelets.json from the Orbit64 V2
# Writes test/cube/notation/orbit64V2Facelets.json and
# test/cube/notation/orbit64V2FaceletPictures.json from the Orbit64 V2
# reference repository (flix-orbit64, Apache-2.0).
#
# julia scripts/orbit64V2FaceletFixture.jl <path to flix-orbit64>
#
# It copies the slot tables of FORMAT-V2-SLOTS.md (n = 2..15) and the rows of
# test/TestV2FaceletData.flix, which tools/format-v2/facelet_vectors.jl
# generates there. Nothing is computed here. The row count comes from the
# input. Needs only Julia's standard library; the output is deterministic, and
# written the way `biome format` leaves it, so regenerating it changes nothing.
# The first copies the slot tables of FORMAT-V2-SLOTS.md (n = 2..15) and the
# color rows of test/TestV2FaceletData.flix, which
# tools/format-v2/facelet_vectors.jl generates there. The second copies the
# picture rows of test/TestV2FaceletPictureData.flix, which
# tools/format-v2/facelet_picture_vectors.jl generates there (n = 3..15). Nothing
# is computed here. Both inputs are checked for dropped, duplicated and
# unparsed rows, and their row and move sequence counts are pinned below. Needs
# only Julia's standard library; the output is deterministic, and written the
# way `biome format` leaves it, so regenerating it changes nothing.

root = length(ARGS) == 1 ? ARGS[1] : error("usage: julia scripts/orbit64V2FaceletFixture.jl <flix-orbit64 dir>")
read_text(path) = read(joinpath(root, path), String)
Expand Down Expand Up @@ -135,6 +140,77 @@ length(rows) == EXPECTED_ROWS || error("TestV2FaceletData.flix: $(length(rows))
sequences_per_size = Dict(n => count(k -> k[1] == n, keys(rows_by_sequence)) for n in keys(ids_by_size))
sequences_per_size == EXPECTED_SEQUENCES || error("TestV2FaceletData.flix: move sequences per size $(sort(collect(sequences_per_size))), expected $(sort(collect(EXPECTED_SEQUENCES)))")

# ---- test/TestV2FaceletPictureData.flix -----------------------------------------
# (4, "1R", "GGCEAQDCN ORBJFBWXL ...",
# "Jx4QRAuFTAAAE",
# ""),
# A row is three lines. The empty move text is the solved cube.

const BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
picture_pattern = r"^\s*\((\d+), \"([^\"]*)\", \"([^\"]*)\",\n\s*\"([^\"]*)\",\n\s*\"([^\"]*)\"\),?[ \t]*$"m
picture_text = read_text("test/TestV2FaceletPictureData.flix")
picture_tuple_lines = length(collect(eachmatch(r"^\s*\(\d+,"m, picture_text)))
picture_rows = [Row(parse(Int, m.captures[1]), m.captures[2], m.captures[3], m.captures[4], m.captures[5])
for m in eachmatch(picture_pattern, picture_text)]
isempty(picture_rows) && error("TestV2FaceletPictureData.flix: no rows")
# Everything that is not a row must be one of the known lines of the file; a
# stray line (a half row, a note) fails instead of being skipped.
let rest = replace(picture_text, picture_pattern => "")
known = r"^(//.*|mod \w+ \{|\s*///.*|\s*pub def rows\w*\(\): .*|\s*\}\)?|\s*List\.flatten\(.*|\s*rows\w+\(\).*|\s*)$"
for line in split(rest, '\n')
occursin(known, line) || error("TestV2FaceletPictureData.flix: unexpected line: $line")
end
end
length(picture_rows) == picture_tuple_lines ||
error("TestV2FaceletPictureData.flix: $picture_tuple_lines tuple lines, $(length(picture_rows)) parsed as rows")
# The same structure as the color rows: a picture entry per pose, each move
# sequence of a size once for both. The picture entry ids of the registry are
# 4n - 8 (canonical) and 4n - 7 (framed) for n = 3..15; every size has exactly them.
picture_sequences = Dict{Tuple{Int,String},Vector{Int}}()
picture_ids = Dict{Int,Set{Int}}()
for r in picture_rows
cells = length(replace(r.facelets, " " => ""))
n = round(Int, sqrt(cells / 6))
6n^2 == cells || error("picture row of entry $(r.id): $cells facelets are no cube")
all(c -> c == ' ' || occursin(c, BASE64URL), r.facelets * r.canonical) ||
error("picture row of entry $(r.id): a character outside the base64url alphabet")
all(c -> occursin(c, BASE64URL), r.token) && !isempty(r.token) ||
error("picture row of entry $(r.id): token \"$(r.token)\" is not base64url")
r.canonical == "" || length(replace(r.canonical, " " => "")) == cells ||
error("picture row of entry $(r.id): canonical facelets of another length")
r.id in (4n - 8, 4n - 7) || error("picture row of entry $(r.id) at size $n, expected $(4n - 8) or $(4n - 7)")
push!(get!(picture_sequences, (n, r.moves), Int[]), r.id)
push!(get!(picture_ids, n, Set{Int}()), r.id)
end
# A token has the length of its entry, so the rows of one entry agree on it.
token_lengths = Dict{Int,Set{Int}}()
for r in picture_rows
push!(get!(token_lengths, r.id, Set{Int}()), length(r.token))
end
for (id, lengths) in token_lengths
length(lengths) == 1 || error("picture rows of entry $id have tokens of lengths $(sort(collect(lengths)))")
end
sort(collect(keys(picture_ids))) == collect(3:15) ||
error("picture rows for sizes $(sort(collect(keys(picture_ids)))), expected 3:15")
for (n, ids) in picture_ids
ids == Set([4n - 8, 4n - 7]) || error("$(n)x$(n)x$(n): picture rows of entries $(sort(collect(ids))), expected $(4n - 8) and $(4n - 7)")
end
for ((n, moves), ids) in picture_sequences
sort(ids) == [4n - 8, 4n - 7] ||
error("$(n)x$(n)x$(n): picture moves \"$moves\" have rows for entries $(sort(ids)), expected $(4n - 8) and $(4n - 7) once each")
end
# Pinned like the color counts: dropping both rows of a move sequence leaves the
# structure above intact. Change them together with the input.
const EXPECTED_PICTURE_SEQUENCES = Dict(3 => 41, 4 => 4, 5 => 5, 6 => 4, 7 => 5, 8 => 3,
9 => 4, 10 => 3, 11 => 4, 12 => 3, 13 => 4, 14 => 3, 15 => 4)
const EXPECTED_PICTURE_ROWS = 2 * sum(values(EXPECTED_PICTURE_SEQUENCES))
EXPECTED_PICTURE_ROWS == 174 || error("the pinned picture counts add up to $EXPECTED_PICTURE_ROWS rows, expected 174")
length(picture_rows) == EXPECTED_PICTURE_ROWS ||
error("TestV2FaceletPictureData.flix: $(length(picture_rows)) rows, expected $EXPECTED_PICTURE_ROWS")
picture_per_size = Dict(n => count(k -> k[1] == n, keys(picture_sequences)) for n in keys(picture_ids))
picture_per_size == EXPECTED_PICTURE_SEQUENCES ||
error("TestV2FaceletPictureData.flix: move sequences per size $(sort(collect(picture_per_size))), expected $(sort(collect(EXPECTED_PICTURE_SEQUENCES)))")

# ---- JSON ----------------------------------------------------------------------

function quoted(text::AbstractString)
Expand Down Expand Up @@ -196,6 +272,20 @@ $row_text
}
"""

target = joinpath(@__DIR__, "..", "test", "cube", "notation", "orbit64V2Facelets.json")
write(target, json)
println("wrote ", length(rows), " rows and the slot tables of ", length(slots), " sizes")
picture_json = """
{
"source": $(quoted("$source_name: test/TestV2FaceletPictureData.flix (generated there by tools/format-v2/facelet_picture_vectors.jl)")),
"generatedBy": "scripts/orbit64V2FaceletFixture.jl. Do not edit.",
"rowText": "id: registry entry; moves: layer turns (depth, face, suffix) and whole-cube turns x y z from the solved cube, empty for the solved cube; facelets: what the entry writes for the cube, one base64url symbol per facelet; token: its V2 token; canonical: the reference pose of that cube when the entry's facelets differ from it, else empty.",
"rowCount": $(length(picture_rows)),
"rows": [
$(join([row_object(r) for r in picture_rows], ",\n"))
]
}
"""

notation = joinpath(@__DIR__, "..", "test", "cube", "notation")
write(joinpath(notation, "orbit64V2Facelets.json"), json)
write(joinpath(notation, "orbit64V2FaceletPictures.json"), picture_json)
println("wrote ", length(rows), " color rows and the slot tables of ", length(slots), " sizes, and ",
length(picture_rows), " picture rows")
31 changes: 26 additions & 5 deletions src/cube/notation/Orbit64V2.res
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,31 @@ let unrankState = (n, model, pose, value) => {

let notAV2Token = "not a known V2 state token"

// No array of a cube has more than 42 entries (the center orbits of a 15x15x15).
// A longer one is read only up to this many entries plus one: it is no state
// either way, and a hostile length costs no time or memory.
let maxEntries = 64

// A copy of the arrays and numbers of a cube, read once and by index, so that
// holes become undefined (and fail validation) and a getter cannot answer
// differently between validation and ranking.
let readArray = values => Array.fromInitializer(~length=Array.length(values), i => values->get(i))
// differently between validation and ranking. The length is read once and only
// a non-negative integer counts: anything else (an object with a valueOf, NaN,
// no length at all) reads as one undefined entry, which fits no layout. Only a
// primitive number is kept as a number: any other entry (an object whose
// valueOf answers differently each time, a Number object, a bigint) becomes
// undefined, so the copy is plain data and no caller code runs on it.
let readEntries = (values, entry) => {
let length = Array.length(values)
// isInt alone would convert a bigint or symbol (and throw), so look at the type first.
let size =
Type.typeof(length) == #number && isInt(length) && length >= 0
? Math.Int.min(length, maxEntries + 1)
: 1
Array.fromInitializer(~length=size, i => entry(values->get(i)))
}

let primitive = (x: int): int => Type.typeof(x) == #number ? x : Obj.magic(undefined)
let readArray = values => readEntries(values, primitive)

let snapshot = (cube: cube): cube => {
let {cornerPieces, cornerTwists, midgePieces, midgeFlips, wings, centers, rotations, frame} = cube
Expand All @@ -404,10 +425,10 @@ let snapshot = (cube: cube): cube => {
cornerTwists: readArray(cornerTwists),
midgePieces: readArray(midgePieces),
midgeFlips: readArray(midgeFlips),
wings: readArray(wings)->Array.map(readArray),
centers: readArray(centers)->Array.map(readArray),
wings: readEntries(wings, readArray),
centers: readEntries(centers, readArray),
rotations: readArray(rotations),
frame,
frame: primitive(frame),
}
}

Expand Down
Loading
Loading