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
26 changes: 26 additions & 0 deletions encoding/hex/decode.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,33 @@ fn hex_value(code_unit : Int) -> Int {
/// Both lowercase and uppercase hexadecimal characters are accepted. Raises
/// `Malformed` if the input length is odd or any character is not a
/// hexadecimal digit.
#cfg(not(any(target="native", target="wasm")))
pub fn decode(text : StringView) -> Bytes raise Malformed {
decode_scalar(text)
}

///|
/// Decodes a hexadecimal string into bytes.
///
/// Both lowercase and uppercase hexadecimal characters are accepted. Raises
/// `Malformed` if the input length is odd or any character is not a
/// hexadecimal digit.
#cfg(any(target="native", target="wasm"))
pub fn decode(text : StringView) -> Bytes raise Malformed {
match decode_v128(text) {
Some(bytes) => bytes
// Not handled by the fast path: short, odd-length, or invalid input.
// The scalar decoder is the single source of the `Malformed` rules.
None => decode_scalar(text)
}
}

///|
/// Retained on every backend: the linear-memory backends reach it only from
/// the differential tests and the fast path's rejections, but it is the
/// implementation everywhere else.
#warnings("-unused_value")
fn decode_scalar(text : StringView) -> Bytes raise Malformed {
if text.length() % 2 != 0 {
raise Malformed(text)
}
Expand Down
121 changes: 121 additions & 0 deletions encoding/hex/decode_v128.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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 hexadecimal decoding for the linear-memory backends.
//
// A MoonBit `String` is UTF-16, so a block loads four vectors of eight code
// units and narrows them to thirty-two ASCII characters, which decode to
// sixteen bytes. The narrow reads its lanes as signed and saturates, so a
// code unit in `0x0100..=0x7FFF` becomes 255 and one in `0x8000..=0xFFFF`
// -- surrogate halves included -- becomes 0. Neither is a hex digit, so the
// validity check rejects both and no non-ASCII code unit can masquerade as
// a hex digit by way of its low byte.
//
// The fast path only handles even-length input made of hex digits. Anything
// else defers to `decode_scalar`, which remains the single definition of the
// `Malformed` rules.

///|
/// True when every lane holds an ASCII hex digit (`0-9`, `a-f`, `A-F`).
#cfg(any(target="native", target="wasm"))
fn hex_valid_v128(ascii : V128) -> Bool {
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 lower = @v128.v128_and_(
@v128.i8x16_ge_u(ascii, @v128.i8x16_splat(b'a')),
@v128.i8x16_le_u(ascii, @v128.i8x16_splat(b'f')),
)
let upper = @v128.v128_and_(
@v128.i8x16_ge_u(ascii, @v128.i8x16_splat(b'A')),
@v128.i8x16_le_u(ascii, @v128.i8x16_splat(b'F')),
)
@v128.i8x16_all_true(@v128.v128_or_(digit, @v128.v128_or_(lower, upper)))
}

///|
/// Maps sixteen ASCII hex digits to their nibble values. Only meaningful
/// after `hex_valid_v128` accepted the lanes: `- '0'` handles digits, and a
/// further masked `- 39` (lowercase) or `- 7` (uppercase) shifts the letter
/// ranges onto 10..=15. The adjustments are wrapping adds of the two's
/// complements because `i8x16_add` wraps.
#cfg(any(target="native", target="wasm"))
fn hex_nibbles_v128(ascii : V128) -> V128 {
let lower = @v128.v128_and_(
@v128.i8x16_ge_u(ascii, @v128.i8x16_splat(b'a')),
@v128.i8x16_splat(217), // -39 mod 256
)
let upper = @v128.v128_and_(
@v128.v128_and_(
@v128.i8x16_ge_u(ascii, @v128.i8x16_splat(b'A')),
@v128.i8x16_le_u(ascii, @v128.i8x16_splat(b'F')),
),
@v128.i8x16_splat(249), // -7 mod 256
)
@v128.i8x16_add(
@v128.i8x16_add(ascii, @v128.i8x16_splat(208)), // -48 mod 256
@v128.v128_or_(lower, upper),
)
}

///|
/// Decodes even-length all-hex text. Returns `None` when the fast path does
/// not apply (short, odd-length, or invalid input), 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 one full block the scalar decoder wins outright.
guard length >= 32 && length % 2 == 0 else { return None }
let out = FixedArray::make(length / 2, b'\x00')
let src = text.data()
let base = text.start_offset()
let mut index = 0
let mut written = 0
// subtraction, so that the bound cannot wrap on a very long input
while length - index >= 32 {
let offset = base + index
let ascii0 = @v128.i8x16_narrow_i16x8_u(
@v128.v128_load_i16x8(src, offset),
@v128.v128_load_i16x8(src, offset + 8),
)
let ascii1 = @v128.i8x16_narrow_i16x8_u(
@v128.v128_load_i16x8(src, offset + 16),
@v128.v128_load_i16x8(src, offset + 24),
)
guard hex_valid_v128(ascii0) && hex_valid_v128(ascii1) else { return None }
let n0 = hex_nibbles_v128(ascii0)
let n1 = hex_nibbles_v128(ascii1)
// high nibbles sit in the even lanes, low nibbles in the odd lanes
let hi = @v128.i8x16_shuffle(
n0, n1, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30,
)
let lo = @v128.i8x16_shuffle(
n0, n1, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31,
)
@v128.v128_store(out, written, @v128.v128_or_(@v128.i8x16_shl(hi, 4), lo))
index += 32
written += 16
}
while index < length {
let hi = hex_value(text.unsafe_get(index).to_int())
let lo = hex_value(text.unsafe_get(index + 1).to_int())
guard hi >= 0 && lo >= 0 else { return None }
out[written] = ((hi << 4) | lo).to_byte()
index += 2
written += 1
}
Some(out.unsafe_reinterpret_as_bytes())
}
18 changes: 18 additions & 0 deletions encoding/hex/encode.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,25 @@ const HEX_DIGITS : Bytes = b"0123456789abcdef"
/// Encodes bytes as a lowercase hexadecimal string.
///
/// The returned string has length `2 * bytes.length()`.
#cfg(not(any(target="native", target="wasm")))
pub fn encode(bytes : BytesView) -> String {
encode_scalar(bytes)
}

///|
/// Encodes bytes as a lowercase hexadecimal string.
///
/// The returned string has length `2 * bytes.length()`.
#cfg(any(target="native", target="wasm"))
pub fn encode(bytes : BytesView) -> String {
encode_v128(bytes)
}

///|
/// Retained on every backend: the linear-memory backends reach it only from
/// the differential tests, but it is the implementation everywhere else.
#warnings("-unused_value")
fn encode_scalar(bytes : BytesView) -> String {
// size_hint is measured in bytes, and each of the 2n output code units
// occupies two bytes in the builder's UTF-16 buffer
let builder = StringBuilder(size_hint=4 * bytes.length())
Expand Down
98 changes: 98 additions & 0 deletions encoding/hex/encode_v128.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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 hexadecimal encoding for the linear-memory backends.
//
// Each iteration turns sixteen source bytes into thirty-two hex characters:
// split every byte into its high and low nibble, interleave the nibbles into
// output order, and convert each nibble to ASCII with a single register-
// resident table lookup (`i8x16_swizzle`). Because a MoonBit `String` is
// UTF-16, the thirty-two ASCII characters are widened to sixty-four bytes
// before being stored.

///|
/// V128 loads require `FixedArray[Byte]`; these types share a representation
/// on the linear-memory backends.
#cfg(any(target="native", target="wasm"))
fn unsafe_fixedarray_from_bytes(bytes : Bytes) -> FixedArray[Byte] = "%identity"

///|
/// The sixteen hex digits as swizzle-table lanes: lane `n` holds the ASCII
/// code of the lowercase digit for nibble value `n`.
#cfg(any(target="native", target="wasm"))
fn hex_digits_v128() -> V128 {
@v128.i8x16_const(
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102,
)
}

///|
/// Writes one ASCII character as a little-endian UTF-16 code unit.
#cfg(any(target="native", target="wasm"))
#inline
fn write_code_unit(out : FixedArray[Byte], offset : Int, code : Byte) -> Unit {
out[offset] = code
out[offset + 1] = 0
}

///|
#cfg(any(target="native", target="wasm"))
fn encode_v128(bytes : BytesView) -> String {
let length = bytes.length()
// The vector stores are not bounds checked, so the UTF-16 byte buffer
// must be sized without wrapping. Its four bytes per source byte overflow
// sooner than the string itself does, so the scalar encoder -- which
// builds the string without ever materializing that buffer -- takes over
// from here.
guard length <= 0x1FFF_FFFF else { return encode_scalar(bytes) } // Int::MAX / 4
// 2 output code units per byte, 2 bytes per UTF-16 code unit
let out = FixedArray::make(length * 4, b'\x00')
let src = unsafe_fixedarray_from_bytes(bytes.data())
let base = bytes.start_offset()
let end = base + length
let table = hex_digits_v128()
let mut index = base
let mut written = 0
// written as a subtraction so that a view sitting at the very end of a
// large buffer cannot wrap the bound into accepting a short block
while end - index >= 16 {
let data = @v128.v128_load(src, index)
let hi = @v128.i8x16_shr_u(data, 4)
let lo = @v128.v128_and_(data, @v128.i8x16_splat(0x0F))
// interleave: source byte k produces digits at output positions 2k
// (high nibble) and 2k + 1 (low nibble)
let first = @v128.i8x16_shuffle(
hi, lo, 0, 16, 1, 17, 2, 18, 3, 19, 4, 20, 5, 21, 6, 22, 7, 23,
)
let second = @v128.i8x16_shuffle(
hi, lo, 8, 24, 9, 25, 10, 26, 11, 27, 12, 28, 13, 29, 14, 30, 15, 31,
)
let d0 = @v128.i8x16_swizzle(table, first)
let d1 = @v128.i8x16_swizzle(table, second)
@v128.v128_store(out, written, @v128.i16x8_extend_low_i8x16_u(d0))
@v128.v128_store(out, written + 16, @v128.i16x8_extend_high_i8x16_u(d0))
@v128.v128_store(out, written + 32, @v128.i16x8_extend_low_i8x16_u(d1))
@v128.v128_store(out, written + 48, @v128.i16x8_extend_high_i8x16_u(d1))
index += 16
written += 64
}
while index < end {
let n = src[index].to_int()
write_code_unit(out, written, HEX_DIGITS[(n >> 4) & 0x0F])
write_code_unit(out, written + 2, HEX_DIGITS[n & 0x0F])
index += 1
written += 4
}
out.unsafe_reinterpret_as_bytes().to_unchecked_string()
}
14 changes: 14 additions & 0 deletions encoding/hex/moon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,18 @@ import {
"moonbitlang/core/buffer",
"moonbitlang/core/builtin",
"moonbitlang/core/debug",
"moonbitlang/core/v128",
}

import {
"moonbitlang/core/quickcheck",
} for "test"

import {
"moonbitlang/core/bench",
"moonbitlang/core/test",
} for "wbtest"

// The v128 import is only reachable from the linear-memory backends.

warnings = "-29"
Loading
Loading