Rust std読み会 ascii編

Module std::ascii を調べました。その過程でASCII文字についても調べました。

行ったこと

  • 参加: kaito_tateyama, いかなご さん
  • 時間: 5/16(火) 20:30 - 22:36 (23:30終了予定、早めに終わったので切り上げた)
  • 形態: オンライン、discordで通話 + hackmdでドキュメント共有
  • 内容: RustのModule std::ascii を眺めてASCII文字列について調べた

技術

Module std::ascii について

link: https://doc.rust-lang.org/std/ascii/index.html

Module std::ascii にはASCII文字や文字列に対しての処理が含まれる。

以下の3つの要素で構成されている

  • Structs
    • EscapeDefault
  • Traits
    • AsciiExt: Deprecated
  • Functions
    • escape_default

ソースコードは主に rust/library/std/src/ascii.rsrust/library/core/src/ascii.rs を参照した。

AsciiExt に関して

なぜDeprecatedなのか、代替は

u8str などにメソッドとして実質的に同じものが実装されているから。

rust/library/std/src/ascii.rs にNOTEとして書かれている。

そのため、代替としては実質的に同じメソッドを探して使えば良い。例えば Primitiveの char には is_ascii がある

escape_default に関して

link: https://doc.rust-lang.org/std/ascii/fn.escape_default.html

u8 の引数をとって、struct EscapeDefault を返す。EscapeDefault は実質イテレータのようなものなので、以下のようなコードが書ける。

use std::ascii;

let escaped = ascii::escape_default(b'0').next().unwrap();
assert_eq!(b'0', escaped);

// ref: https://doc.rust-lang.org/std/ascii/fn.escape_default.html

直感的には「1つの文字しか引数に取らないのにイテレータを返したら、一個nextで消費しておしまいにならない?」という疑問が湧くが、以下のように最大長さ4のイテレータが返る。

let mut escaped = ascii::escape_default(b'\x9d');

assert_eq!(b'\\', escaped.next().unwrap());
assert_eq!(b'x', escaped.next().unwrap());
assert_eq!(b'9', escaped.next().unwrap());
assert_eq!(b'd', escaped.next().unwrap());

// ref: https://doc.rust-lang.org/std/ascii/fn.escape_default.html

詳しいルールはC++11やC系の言語を参考に決められていて、ドキュメントに書かれている。

また一見すると、以下の2つの例は b'\''b'"' というbackslashの有無に関して異なるものが同じような結果になっていて不思議に感じたが、これは表記上single quoteをescapeする必要があるからだということに気づいて納得した。

// ' single quote
let mut escaped = ascii::escape_default(b'\'');

assert_eq!(b'\\', escaped.next().unwrap());
assert_eq!(b'\'', escaped.next().unwrap());

// " double quote
let mut escaped = ascii::escape_default(b'"');

assert_eq!(b'\\', escaped.next().unwrap());
assert_eq!(b'"', escaped.next().unwrap());

実装を眺める。会の時点ではこのように直で実装されていた。
長さ4の固定長の配列をデータとして持っておいて、len で長さを示すという設計になっている。確かにここは小さいので可変長配列にする必要はなさそう。

#[derive(Clone)]
pub struct EscapeDefault {
    range: Range<u8>,
    data: [u8; 4],
}

pub fn escape_default(c: u8) -> EscapeDefault {
    let (data, len) = match c {
        b'\t' => ([b'\\', b't', 0, 0], 2),
        b'\r' => ([b'\\', b'r', 0, 0], 2),
        b'\n' => ([b'\\', b'n', 0, 0], 2),
        b'\\' => ([b'\\', b'\\', 0, 0], 2),
        b'\'' => ([b'\\', b'\'', 0, 0], 2),
        b'"' => ([b'\\', b'"', 0, 0], 2),
        b'\x20'..=b'\x7e' => ([c, 0, 0, 0], 1),
        _ => {
            let hex_digits: &[u8; 16] = b"0123456789abcdef";
            ([b'\\', b'x', hex_digits[(c >> 4) as usize], hex_digits[(c & 0xf) as usize]], 4)
        }
    };

    return EscapeDefault { range: 0..len, data };
}

// ref: https://doc.rust-lang.org/src/core/ascii.rs.html#91-108

会の後に最新commit を眺めると以下のように変わっていた。

#[derive(Clone)]
pub struct EscapeDefault(escape::EscapeIterInner<4>);

pub fn escape_default(c: u8) -> EscapeDefault {
    let mut data = [0; 4];
    let range = escape::escape_ascii_into(&mut data, c);
    EscapeDefault(escape::EscapeIterInner::new(data, range))
}

// escape::escape_ascii_into https://github.com/rust-lang/rust/blob/e77366b57b799dfa3ce1fcb850c068723a3213ee/library/core/src/escape.rs#L10-L27
pub(crate) fn escape_ascii_into(output: &mut [u8; 4], byte: u8) -> Range<u8> {
    let (data, len) = match byte {
        b'\t' => ([b'\\', b't', 0, 0], 2),
        b'\r' => ([b'\\', b'r', 0, 0], 2),
        b'\n' => ([b'\\', b'n', 0, 0], 2),
        b'\\' => ([b'\\', b'\\', 0, 0], 2),
        b'\'' => ([b'\\', b'\'', 0, 0], 2),
        b'"' => ([b'\\', b'"', 0, 0], 2),
        b'\x20'..=b'\x7e' => ([byte, 0, 0, 0], 1),
        _ => {
            let hi = HEX_DIGITS[usize::from(byte >> 4)];
            let lo = HEX_DIGITS[usize::from(byte & 0xf)];
            ([b'\\', b'x', hi, lo], 4)
        }
    };
    *output = data;
    0..(len as u8)
}

リファクタリングされて、特にルールの Any other chars are given hex escapes of the form ‘\xNN’. に対応する部分が分かりやすくなっている。

EscapeDefault に関して

link: https://doc.rust-lang.org/std/ascii/struct.EscapeDefault.html

function escape_default の返り値として struct EscapeDefault が生成される。

Iteratorsize_hint

EscapeDefault に対する Iterator
を見ていると、Itemnext は知っていたが size_hint というのを見つけた。

size_hint() is primarily intended to be used for optimizations such as reserving space for the elements of the iterator, but must not be trusted to e.g., omit bounds checks in unsafe code. An incorrect implementation of size_hint() should not lead to memory safety violations.
size_hint のドキュメント より

size_hint は最適化周りで役に立つらしい。

fmt::Display の unsafe

link: https://doc.rust-lang.org/src/core/ascii.rs.html#137-144

以下のようにunsafeが使われていたので眺めていると、from_utf8_unchecked を使用しているが、コメントにもあるように escape_default 関数の返り値は \x9d\ x 9 d のようにそれぞれUTF-8な文字なのでSafetyになる。

impl fmt::Display for EscapeDefault {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // SAFETY: ok because `escape_default` created only valid utf-8 data
        f.write_str(unsafe {
            from_utf8_unchecked(&self.data[(self.range.start as usize)..(self.range.end as usize)])
        })
    }
}

会の後で 最新commit を眺めたところ、そもそも EscapeDefault の定義を変えたので以下のようにunsafeを使わない形になっていた。面白い。

impl fmt::Display for EscapeDefault {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.0.as_str())
    }
}

会では便利さからドキュメントから飛べるリンクを見ていいけど終わった後に最新コミット確認するようにしたら違いが分かって楽しいかも。

fmt::Debugdebug_structfinish_non_exhaustive

  • debug_struct
    • structの名称を文字列として渡すと DebugStruct 構造体を生成する。
    • メタっぽい動きだなと思った。
  • finish_non_exhaustive
    • .. の表示ができるやつ。構造体について debug_struct にチェインでfield を指定したもの以外を .. 表示にできる。

ASCIIについて

man ascii

man asciiのNOTES > Historyの項目について
macOSやFreeBSDは一行 An ascii manual page appeared in Version 7 of AT&T UNIX. とだけ書かれていたが、Ubuntu 22.04には以下のように書かれていた

NOTES
History
An ascii manual page appeared in Version 7 of AT&T UNIX.

On older terminals, the underscore code is displayed as a left arrow, called backar‐
row, the caret is displayed as an up-arrow and the vertical bar has a  hole  in  the
middle.

Uppercase  and lowercase characters differ by just one bit and the ASCII character 2
differs from the double quote by just one bit, too.  That made it much easier to en‐
code characters mechanically or with a non-microcontroller-based electronic keyboard
and that pairing was found on old teletypes.

The ASCII standard was published by the United States of America Standards Institute
(USASI) in 1968.

-- man asciiより引用

That made it much easier to en‐
code characters mechanically or with a non-microcontroller-based electronic keyboard
and that pairing was found on old teletypes.

これどういうこと?と思ったけどどう当時の状況を調べたらいいか分からなかった。

キーボード的な話

“the ASCII character 2 differs from the double quote by just one bit” → 文字の2(0x32)と"(0x22)が1bit違う
Apple II keyboard がそうかも?

Apple II keyboard を眺めていた。チルダが変なところにあったり、ReturnとEnterが別のキーで面白い。

その他ASCIIっぽい話題

CRは文字列先頭、LFはライン送り(改行)の役割だった

IIJ技術研究所のASCIIについての記事 によると、仕様でASCIIはどこがどの役割というのはそんなに明確に決まっているわけでもなさそう。日本のJIS規格でbackslashが円マークに割り当てられるところの話とか面白かった。

DELが0x7fである理由 (Wikipediaより)

この制御文字だけ先頭部分になく最後にある理由は、パンチテープへの記録は上書きが出来ないため、削除する際には全てに穴を空けることで対応できるというところからきている(1111111は全てに穴の開いた状態を示す)

その他

assert_matches に関して

前回のstd読み会 と同様に RustのstdのModules を眺めたところ、assert_matches というExperimentalなModulesを発見した。

This is a nightly-only experimental API. (assert_matches #82775)

とあったので Issue#82775 を眺めた。

2023/05/16時点でのExample Macro std::assert_matches::assert_matches

#![feature(assert_matches)]

use std::assert_matches::assert_matches;

let a = 1u32.checked_add(2);
let b = 1u32.checked_sub(2);
assert_matches!(a, Some(_));
assert_matches!(b, None);

let c = Ok("abc".to_string());
assert_matches!(c, Ok(x) | Err(x) if x.len() < 100);

Macro std::assert_eq と似ている。

// Macro std::assert_eqのExample
// ref: https://doc.rust-lang.org/std/macro.assert_eq.html
let a = 3;
let b = 1 + 2;
assert_eq!(a, b);

assert_eq!(a, b, "we are testing addition with {} and {}", a, b);

assert_eq!PartialEq を実装した2つの要素を比較できて、 assert_matches! はEnumを直接比較できる点が異なる。今まで assert_eq!(x.is_some(), true); とかやってたのが assert_matches!(x, Some(_)); と書けるようになりそう。

libstd?という表現

Issue#82775 に言及していた murarth/assert_matches Issue#2 を眺めていた。

Partial implementations are in unstable libstd: rust-lang/rust#82775.

というコメントがあり、"libstd"という表現が気になった。

Rust RFCを見ると2014年に 0040-libstd-facade で “libstd” という表現が使用されている。

(あんまり読めてないけど) 当時libminiというprimitiveなものやliblibc, liballocなどをまとめてlibstdとして一本化する変更があったらしい?
元々 libXXX みたいなのが散らばってたのをまとめたのではないか?という結論になった。おそらくlibstdは昔の呼び方で、現在は std 呼びでいい気がする…?

どこから読むか候補決め

  • prelude
  • assert_matches!
  • net
  • pin
  • task
  • future
  • ascii, char
    • 文字列について予習する必要があるかも
  • simd

非同期やるなら、順番としては pin -> task -> future

路線としては、

  • 同期非同期の沼
  • 文字列の沼 👈
  • ネットワークの沼
  • simdの沼

今回は相談して文字列の沼が一番趣味っぽいしこれにしようという話になった。
この機会に文字列周りの話題(UTF-8などの文字コードとか)に少し詳しくなりたい。

決めたこと

  • お互いに文字列に対してモチベが切れたら言う。撤退して別のをしましょう。
  • 撤退はちゃんとする
    • どこまで理解したか、疑問はこれ、とか振り返りはする

録音はした方がいいかも?

  • 話した内容を忘れてしまう
  • 記録は進捗のために存在すべきで、忘れないための記録は録音 + 自動での文字起こしでいいのではないか(文字起こししたものを2者間で共有)

追加で調べたい

  • DoubleEndedIterator
    • std::ascii::EscapeDefault に実装されているトレイトを眺めていたら見つけた。
  • Iteratorについてくる関数たち
    • Iteratorについてよく知らないので構造的にまとめたい
  • shadowingは型が異なってもいける
    • このあたり実装見たい

次回

文字列シリーズということで、Primitiveの char をやろう!
文字列シリーズの目標は「UTF-8をふんわり理解する」です。