// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Vectorized Base64 decoding for the linear-memory backends. The character
// classification uses the perfect hash on the high nibble described in
// https://mcyoung.xyz/2023/11/27/simd-base64/.
//
// A MoonBit `String` is UTF-16, so a block loads two vectors of eight code
// units to obtain sixteen characters, which yield twelve decoded bytes.
//
// The fast path only handles canonical input: no whitespace, a length that is
// a multiple of four, and characters drawn from the Base64 alphabet. Anything
// else defers to `decode_scalar`, which remains the single definition of the
// padding and trailing-bit rules.
///|
/// Offsets added to a Base64 character to reach its sextet, indexed by the
/// perfect hash `(c >> 4) - (c == '/')`: 1 -> `/`, 2 -> `+`, 3 -> digits,
/// 4..=5 -> uppercase, 6..=7 -> lowercase. Values are modulo 256 because
/// `i8x16_add` wraps.
#cfg(any(target="native", target="wasm"))
fn decode_offsets_v128() -> V128 {
@v128.i8x16_const(0, 16, 19, 4, 191, 191, 185, 185, 0, 0, 0, 0, 0, 0, 0, 0)
}
///|
/// True when every lane holds a character of the standard Base64 alphabet.
/// Padding and whitespace deliberately fail this test so the caller falls
/// back to the scalar decoder.
#cfg(any(target="native", target="wasm"))
fn decode_valid_v128(ascii : V128) -> Bool {
let upper = @v128.v128_and_(
@v128.i8x16_ge_u(ascii, @v128.i8x16_splat(b'A')),
@v128.i8x16_le_u(ascii, @v128.i8x16_splat(b'Z')),
)
let lower = @v128.v128_and_(
@v128.i8x16_ge_u(ascii, @v128.i8x16_splat(b'a')),
@v128.i8x16_le_u(ascii, @v128.i8x16_splat(b'z')),
)
let digit = @v128.v128_and_(
@v128.i8x16_ge_u(ascii, @v128.i8x16_splat(b'0')),
@v128.i8x16_le_u(ascii, @v128.i8x16_splat(b'9')),
)
let plus = @v128.i8x16_eq(ascii, @v128.i8x16_splat(b'+'))
let slash = @v128.i8x16_eq(ascii, @v128.i8x16_splat(b'/'))
@v128.i8x16_all_true(
@v128.v128_or_(
@v128.v128_or_(upper, lower),
@v128.v128_or_(digit, @v128.v128_or_(plus, slash)),
),
)
}
///|
/// Turns sixteen Base64 characters into twelve decoded bytes, held in the low
/// twelve lanes of the result.
#cfg(any(target="native", target="wasm"))
fn decode_block_v128(ascii : V128) -> V128 {
// `i8x16_eq` yields all-ones, i.e. -1, so adding it subtracts one for '/'.
let hash = @v128.i8x16_add(
@v128.i8x16_shr_u(ascii, 4),
@v128.i8x16_eq(ascii, @v128.i8x16_splat(b'/')),
)
let sextets = @v128.i8x16_add(
ascii,
@v128.i8x16_swizzle(decode_offsets_v128(), hash),
)
// Fuse each group of four sextets into a 24-bit value held in the low three
// bytes of its 32-bit lane.
let groups = @v128.v128_or_(
@v128.v128_or_(
@v128.i32x4_shl(
@v128.v128_and_(sextets, @v128.i32x4_splat(0x0000003F)),
18,
),
@v128.i32x4_shl(
@v128.v128_and_(sextets, @v128.i32x4_splat(0x00003F00)),
4,
),
),
@v128.v128_or_(
@v128.i32x4_shr_u(
@v128.v128_and_(sextets, @v128.i32x4_splat(0x003F0000)),
10,
),
@v128.i32x4_shr_u(
@v128.v128_and_(sextets, @v128.i32x4_splat(0x3F000000)),
24,
),
),
)
// Emit each lane's three payload bytes most-significant first.
@v128.i8x16_shuffle(
groups, groups, 2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, 0, 0, 0, 0,
)
}
///|
/// Decodes canonical Base64 text. Returns `None` when the input is not
/// canonical, in which case the caller must use the scalar decoder.
#cfg(any(target="native", target="wasm"))
fn decode_v128(text : StringView) -> Bytes? {
let length = text.length()
// Below this the scalar decoder wins outright: one block needs sixteen
// characters plus a four-character tail group.
guard length >= 32 && length % 4 == 0 else { return None }
let padding = if text.unsafe_get(length - 1) != b'='.to_uint16() {
0
} else if text.unsafe_get(length - 2) != b'='.to_uint16() {
1
} else {
2
}
let out = FixedArray::make(length / 4 * 3 - padding, b'\x00')
let src = text.data()
// The final group may carry padding, so it is always left to the scalar
// decoder; everything before it must be plain alphabet.
let body = length - 4
let base = text.start_offset()
let mut index = 0
let mut written = 0
while body - index >= 16 && out.length() - written >= 16 {
let offset = base + index
let ascii = @v128.i8x16_narrow_i16x8_u(
@v128.v128_load_i16x8(src, offset),
@v128.v128_load_i16x8(src, offset + 8),
)
guard decode_valid_v128(ascii) else { return None }
@v128.v128_store(out, written, decode_block_v128(ascii))
index += 16
written += 12
}
// Remaining whole groups of the body, still guaranteed padding-free.
while index < body {
let mut group = 0
for lane in 0..<4 {
let value = base64_value(text.unsafe_get(index + lane).to_int())
guard value >= 0 else { return None }
group = (group << 6) | value
}
out[written] = (group >> 16).to_byte()
out[written + 1] = (group >> 8).to_byte()
out[written + 2] = group.to_byte()
index += 4
written += 3
}
// Reuse the scalar decoder for the final group so the padding and
// trailing-bit rules live in exactly one place.
let tail_bytes = decode_scalar(text[body:], false) catch { _ => return None }
guard tail_bytes.length() == out.length() - written else { return None }
for i, byte in tail_bytes {
out[written + i] = byte
}
Some(out.unsafe_reinterpret_as_bytes())
}