なんだこれは

はてなダイアリーから移転しました。

ワルツとは?

たまにミラクル起す

ワルツとは?

私とワルツを という歌があって、ふっと気がついたのですが、ワルツは3拍子ですよね。私と貴方しか出てこないが他に登場人物がいるんじゃないかと。

そう考えると、 動くのを止めた時計 や 鳥達が落ちる とかは死を表現しているように見えますよね。 死別した奥さんでしょうか。 奇妙な晩餐 は お葬式かN周忌でしょうか。 失しなう時がいつか来る事も知っている というのもかなりしっくりきます。

優しいと怖いのは、あれですね。 優しいから奥さんに配慮してしまって私を断わるからですね。

scala で gzip解凍とXML操作

scala3 で

scala3 やってみた

JDK のバージョン

まず、scala3 を動かすには Java 11 以上とされているので、インストールしているJDK を調べる

$ /usr/libexec/java_home -V
Matching Java Virtual Machines (5):
    21.0.8 (x86_64) "Eclipse Adoptium" - "OpenJDK 21.0.8" /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
    21.0.1 (x86_64) "Oracle Corporation" - "Oracle GraalVM 21.0.1+12.1" /Library/Java/JavaVirtualMachines/graalvm-jdk-21.0.1+12.1/Contents/Home
    17.0.16 (x86_64) "Eclipse Adoptium" - "OpenJDK 17.0.16" /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
    11.0.28 (x86_64) "Eclipse Adoptium" - "OpenJDK 11.0.28" /Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home
    1.8.0_392 (x86_64) "Eclipse Temurin" - "Eclipse Temurin 8" /Library/Java/JavaVirtualMachines/temurin-8.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home

Java 17 でよさそうなのでこれに変更

$ export JAVA_HOME=$(/usr/libexec/java_home -v 17)

java のバージョンを再確認

$ java -version
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
openjdk version "17.0.16" 2025-07-15
OpenJDK Runtime Environment Temurin-17.0.16+8 (build 17.0.16+8)
OpenJDK 64-Bit Server VM Temurin-17.0.16+8 (build 17.0.16+8, mixed mode)

scala-cli と sbt

scala を動かす方法として簡単なのが scala-cli で プロジェクトをきっちり管理するのが sbt らしい。一旦更新しておいた

cs update

ソースコード

scala3で書くとこうなるんだって感じがある。Javaの資産をうまく使えていて便利。 scala3は本当にライブラリがいくらでもある。すごい

//> using scala "3.7.2"
//> using dep "org.scala-lang.modules::scala-xml:2.4.0"

import java.io.{FileInputStream, InputStreamReader, BufferedReader}
import java.util.zip.GZIPInputStream
import scala.xml.XML

@main def main(args: String*): Unit =
  if args.isEmpty then
    println("Usage: scala-cli run Main.scala -- <path-to-xml.gz>")
  else
    // gzip 展開
    val xmlString =
      val fis = FileInputStream(args(0))
      val gis = GZIPInputStream(fis)
      val br  = BufferedReader(InputStreamReader(gis, "UTF-8"))
      val sb  = new StringBuilder()
      var line: String | Null = null
      while { line = br.readLine(); line != null } do sb.append(line).append("\n")
      br.close()
      sb.toString()

    // XML パース
    val xml = XML.loadString(xmlString)

    // <file-name> タグ抽出
    val files = (xml \\ "file-name").map { node =>
      val contentId = (node \@ "contentId")  // 属性
      val value      = node.text.trim         // タグの値
      (value, contentId)
    }

    // 出力
    println("file-name tags found:")
    files.foreach { case (value, contentId) =>
      println(s"$value,$contentId")
    }

実行

scala-cli run Main.scala -- ~/Downloads/NOTICE.xml.gz

nimとzigはじめました

nim はじめました

教訓

LLM は マイナー言語や最新バージョンなどは把握していないため、LLMに聞くと高確率でハルシネーションしたり、実装してエラー→旧バージョンではこうだった的な嘘を教えてくれます。

環境は intel macOS 15.6.1です。

nim で gzip されたxmlファイルを解凍してパースする

xml を行う方法は 二種類あるらしいが簡単な方を選んだ。

import os, strutils, strformat, zippy, xmlparser, xmltree, options

when isMainModule:
  if paramCount() < 1:
    quit("Usage: nim2 <gzipFile> [csvOutputFile]")

  let gzipFile = paramStr(1)
  let csvFile = if paramCount() >= 2: some(paramStr(2)) else: none(string)

  # GZIPを解凍
  let compressed = readFile(gzipFile)
  let decompressed = uncompress(compressed)

  # XMLを解析
  let xmlDoc = parseXml(decompressed)

  # CSVのヘッダ
  var rows: seq[string] = @["xmlFile,fileName,contentId"]

  # 必要な要素を探索(例: <file-name> 要素から 値 と contentId を取る)
  for node in xmlDoc.findAll("file-name"):
    let fileName = node.innerText().strip()
    let contentId = node.attr("contentId")
    rows.add(&"{gzipFile},{fileName},{contentId}")

  # 出力処理
  if csvFile.isSome:
    writeFile(csvFile.get(), rows.join("\n"))
  else:
    echo rows.join("\n")

これでコンパイルも実行もできた。やったね

nim c nim2.nim # nim という実行ファイルの生成
nim c -r nim2.nim /path/to/NOTICE.xml.gz # 実行ファイルを生成して、引数を与えて実行

クロスコンパイルできる?

nimコンパイラはC言語のソースを生成しているのでC言語のソースから linux用のバイナリを生成するクロスコンパイルすれば、nim もクロスコンパイルできる。 ということは二つの方法がある。

  1. nim コマンドで C言語を生成してクロスコンパイルする
  2. nim コマンドで C言語を生成まで。その後クロスコンパイルする。

ここで zig cc を使うとクロスコンパイルが簡単にできるという情報があったので試してみた。 もちろん、 glibc のバージョンが違って動かないという問題を回避するために musl libc を使おう。

一気にクロスコンパイルで失敗

いろいろ調べた結果これでいけそうなんだがうまくいかない。

CC="zig cc" nim c --cc:clang --os:linux --cpu:amd64 --passC:"-target x86_64-linux-musl" --passL:"-target x86_64-linux-musl -static" nim2.nim

これを実行すると、 musl-linux環境の stddef.h を探しにいくのだけれでども、intel MacOS の方を見てしまう。この問題の解決にこうすればいいとLLMはいうのだが、

  --passC:"-target x86_64-linux-musl --sysroot $(zig libc-path)" \

zig libc-path なんてオプションには対応していなくてエラーがでた。 というところで撤退

じゃあ分割してコンパイル

つぎのコマンドで nimcache: で指定したディレクトリにC 言語に書き出すまで行える。

nim c --cc:clang --os:linux --cpu:amd64 --compileOnly \
  --nimcache:build/c nim2.nim

それからこれでクロスコンパイルします、としたいのですが。

zig cc -target x86_64-linux-musl -static \
  -o nim2 \
  build/c/nim2.nim.c \
  $(find build/c -name '*.c' ! -name 'nim2.nim.c')

nim が提供する nimbase.h のパスを教える必要があります。

nim dump

すると、${HOME}/.choosenim/toolchains/nim-${NIMVERSION}/lib のようなディレクトリが表示されますこのディレクトリに nimbase.h があります。 このディレクトリを I オプションで指定します。

zig cc -target x86_64-linux-musl \
  -I${HOME}/.choosenim/toolchains/nim-2.2.4/lib \
  build/c/nim2.nim.c \
  -o nim2

SIMD命令SSSE3 対策

これで成功すると思ったのですが、次のようなエラーがでました。

'_mm_maddubs_epi16' requires target feature 'ssse3', but would be inlined into function 'adler32_ssse3__pkgZzippyZadler3295simd_u84' that is compiled without support for 'ssse3'ect 219 | nimln_(92); mad1_1 = _mm_maddubs_epi16(bytes1_1, tap1_1); |

これは ssse3 というSIMD 命令 (SSSE3) が必要なのに、ツールチェーン(llvm or zig)がデフォルトでサポートしていないようです。これは、zippy ライブラリ(Nim の gzip/zip ライブラリ)が要求しています。

解決方法としては、二つあります。一つは Zig 側に SSSE3 を有効化する です。もう一つは SIMD サポートを無効化したC言語ソースファイルを nim で生成する の二つです。今回は後者を選びました。

Zig 側に SSSE3 を有効化する

最近のx86_64のCPUでは ssse3 のSIMD命令はサポートされています。そのため、実際には有効にしても問題ないはずです。その場合、-mssse3 オプションで ssse3 を有効にすることもできますし、アーキテクチャを指定してあげる方法もあるらしいです。

zig cc -target x86_64-linux-musl \
  -I${HOME}/.choosenim/toolchains/nim-2.2.4/lib \
  -o nim2l \
  -march=x86_64 -mssse3 \
  build/c/nim2.nim.c $(find build/c -name '*.c' ! -name 'nim2.nim.c')

SIMD サポートを無効化したC言語ソースファイルを nim で生成する

SIMD サポートを無効化したC言語ソースファイルを nim で生成するには、-d:noSimd というオプションで SIMD サポートを無効にできることが多いのです。ですが、zippy は -d:zippyNoSimd というオプションで無効にするようです。

nim c --cc:clang --os:linux --cpu:amd64 --compileOnly \
    -d:zippyNoSimd --nimcache:build/c nim2.nim
zig cc -target x86_64-linux-musl \
  -I${HOME}/.choosenim/toolchains/nim-2.2.4/lib \
  -o nim2 \
  build/c/nim2.nim.c $(find build/c -name '*.c' ! -name 'nim2.nim.c') 

キャッシュチェックエラー対策

これで SIMD サポートを無効化できたのですが次に以下のようなエラーがでました。

build/c/nim2.nim.c:1:1: error: CacheCheckFailed

これは、Zig の クロスコンパイル時キャッシュ検証エラーだそうです。

# nim の不要なソースコードを先に削除
rm -rf build/c
# nim で zippy で SIMD命令を無効にしてCソースコードを生成
nim c --cc:clang --os:linux --cpu:amd64 --compileOnly \
    -d:zippyNoSimd --nimcache:build/c nim2.nim

# zig のキャッシュを削除
rm -rf ~/.cache/zig

# zig でキャッシュを無効化して各ファイルを分割コンパイル
for f in $(find build/c -name '*.c'); do
  zig cc -fno-cache -target x86_64-linux-musl -static \
    -I${HOME}/.choosenim/toolchains/nim-2.2.4/lib -c $f -o ${f%.c}.o
done

# zig で全部をリンク
zig cc -target x86_64-linux-musl -static \
  -o nim2l $(find build/c -name '*.o')

これでなんとかnimとzigでクロスコンパイルできました。

nim はじめました

nim はじめました

教訓

LLM は マイナー言語や最新バージョンなどは把握していないため、LLMに聞くと高確率でハルシネーションしたり、実装してエラー→旧バージョンではこうだった的な嘘を教えてくれます。

環境は intel macOS 15.6.1です。

nim 2.2.4

nim をインストールするとき、 choosenim でインストールした方がいいらしい

homebrewでインストールしてすすめるとnimlsp がパッケージマネージャの nimble でインストールできないので消す

# brew で nim をインストールしている場合 のみ
brew uninstall nim
# インストール
curl https://nim-lang.org/choosenim/init.sh -sSf | sh

指示にしたがって、 ~/.bashrc を編集して読み込み

# インストール確認
nim --version
nimble --version
which nimsuggest
# インストール
nimble install nimlsp

gzip 圧縮されたファイルの解凍

シリーズとして NOTICE.xml.gz ファイルを解凍して、XMLファイルを抽出。そこから情報を取るというものを実装することを題材にしている。

import zlib はダメ

LLMによると、nimでは import zlibで標準添付されている zlibパッケージでgzip解凍できるとの情報があった。これはシステム側の zlib.h と libz.*dylibが必要らしい。

そのまま実行すると import zlibで zlibがみつからないというエラーになった。

nim で C言語に変換しているので、システム側の zlib.h と libz.*dylibがみつからないためかもしれないと判断して、探した。 Intel macOSの新しめのバージョンでそれらは $(xcrun --show-sdk-path)/usr/includel/zlib.h と/usr/lib/libz.1.2.12.dylibだろう。 これにあわせて、インクルードパスとリンカオプションを渡して試した。

nim c -r --passC:"-I$(xcrun --show-sdk-path)/usr/include" --passL:"-lz" src/nim2.nim

しかしこれも同様にだめ。結局 v2.2.4は import zlib で失敗する。 念のためにimport zlibで読み込む zlib.nim がそもそもない。

# インポートするzlib.nim がない
find ~/.choosenim/toolchains/nim-2.2.4/ -name 'zlib*' 

さらに調べると、v2.2.4のnimは、zlibはサポートしていないことを調べてみたら、nim標準の std/compress 等でzip圧縮などはサポートしているが、gzip解凍はサポートしていないとのことがわかった。

import zlibは終了

zippy を選ぶ

gzip展開する方法を調べると外部ライブラリで、zippy などがあるらしい。zippyはnimだけでいいので楽そうということでインストール

# インストール
nimble install zippy

# インストールできたらあるはず
find ~/.nimble/ -type f -name '*zippy*'
import zippy, os, strutils

proc gunzipFile(inputPath, outputPath: string) =
  let compressed = readFile(inputPath)
  let decompressed = uncompress(compressed, dfGzip)
  writeFile(outputPath, decompressed)
  echo "解凍完了: ", outputPath

when isMainModule:
  if paramCount() < 1:
    echo "使い方: ", getAppFilename(), " <input.gz> [output]"
    quit(1)

  let inputFile = paramStr(1)
  let outputFile =
    if paramCount() >= 2:
      paramStr(2)
    else:
      # 拡張子 .gz を削除して出力ファイル名を生成
      if inputFile.endsWith(".gz"):
        #inputFile[0 ..^4]
        inputFile[0 .. inputFile.len - 4]  #
      else:
        inputFile & ".out"

  gunzipFile(inputFile, outputFile)
# コンパイル
nim c src/gunzip.nim
# NOTICE.xml.gz を解凍して NOTICE.xml に保存
src/gunzip ~/Downloads/NOTICE.xml.gz NOTICE.xml

一応できた。

NOTICE.xml.gz から file-nameの値とcontentIdを取り出す Gauche編

NOTICE.xml.gz から file-nameの値とcontentIdを取り出す

NOTICE.xml.gz から file-nameの値とcontentIdを取り出す by gauche

gzip 圧縮された XMLファイル、NOTICE.xml.gz があったとします。それで XML ファイルのトップレベル licenses タグ内に file-name タグがあって、そのプロパティ contentId の値と その file-name の値を抽出してみるとします。

<licenses>
  <file-name contentId="1234567890abcdef">value</file-name>
  <file-name contentId="1234567890abcdef">value</file-name>
  <file-name contentId="1234567890abcdef">value</file-name>
  <file-content contentId="1234567890abcdef"><![CDATA[ここにライセンスファイルの中身]]></file-content>
</licenses>
<!-- contentId はライセンス文の md5ハッシュ値  -->

この NOTICE.xml.gz は AOSP の中で使われているライセンス情報を管理するファイルです。多分検索したら手にはいる。 これをパースしてみよう。

gauche way

gauche だと標準で含まれる rfc.zlib と sxml で gzip 解凍 と xml をサポートしているらしい。

  • gzip 解凍は open-inflating-port の :window-bits 47 でいける。
  • xml は...
    • xml を sxml に変換
    • sxml を xpath ではなく、sxpath で操作
    • sxpath がそれほど強力ではなかったというか使いこなせなかったのでそのあとは、lisp でS式操作でやっつけ
(use rfc.zlib)
(use sxml.ssax)
(use sxml.sxpath)
(use sxml.tools)

(define (gz->raw gz-file raw-file)
  (call-with-input-file gz-file
    (lambda (in-port)
      (call-with-output-file raw-file
      (lambda (out-port)
          (copy-port 
              (open-inflating-port in-port :window-bits 47)
              out-port))))))

(define (read-xml-file filename)
  (call-with-input-file filename
    (lambda (port)
      (ssax:xml->sxml port '()))))

(define (extract-file-names sxml-data)
  ((sxpath '(licenses file-name)) sxml-data))

(define (extract-name-contentId record)
  (let ((content-id-value
     (cadar (filter (lambda (ll) (eq? 'contentId (car ll)))
            (cdadr record))))
     (file-name-value (caddr record)))
    (cons file-name-value content-id-value)))

;;;;

; gzip -d
(gz->raw "NOTICE.xml.gz" "NOTICE.xml")

; S式風xml に変換して file-name 部分を抽出
(define fns 
        (extract-file-names (read-xml-file "NOTICE.xml")))

; あとはS式なのでごにょごにょ
(map extract-name-contentId fns)

へっだーはどこ?

macOS のヘッダーはどこ?

最近の macOSのヘッダがある場所は /usr/include じゃなくて別の場所に隠されているらしい。 ふと真夜中に zlib.h をみたいと思ったときにはこうしましょう。

cd $(xcrun --show-sdk-path)/usr/include

XLSX with Common Lisp Part 3

XLSX with Common Lisp Part 3

link

対応策3 :xlsx パッケージのダウンロード元に

xlsx common-lisp のような検索ワードで検索すると、https://gitlab.common-lisp.net/cungil/xlsx がみつかる。なるほど、これらしい。

このリポジトリ、プロジェクトをみていると、かなり古い。作成日が 2015 年になっており、更新日は7 年前から10 年前になっている。

これは、ここに書いても応答がないだろう。でも、一応、ここまできたので、今までの内容を短く Issue #3 にまとめておいた。

Issue #3 UTF-8 support to use your language character in XLSX package

The xlsx is very nice tool. But some special case, it may not treat some string in excel, if it is not ASCII characters.

why ?

octets-to-string without :external-format assumes that the encoding is latin-1. And you can treat a Latin-1 character as UTF-8, you know. Because latin-1 data in excel are safe to show. But non Latin-1 character is not.

The encoding of Excel XML files generated by Excel-2013, LibreOffice, and Google Stylesheet is UTF-8 without BOM. Furthermore, according to a section "6.2.5 XML usage" Part 2 of ECMA-376 / ISO/IEC 29500 (Office Open specification), the encoding of Excel XML files is limited to UTF-8 with BOM, UTF-8 without BOM, UTF-16 LE or UTF-16 BE. Therefore, it can be determined just by checking the BOM at the beginning of the file.

A simple workaround

Assuming that the xml encoding of the Excel file is UTF-8 you can only add :external-format :utf-8 to the octets-to-string in the get-content function.

(ql:quickload :xlsx) ; load your xlsx package
(in-package :xlsx)   ; go to xlsx package 
                     ; redefine get-entry function
(defun get-entry (name zip)
  (let ((entry (zip:get-zipfile-entry name zip)))
    (when entry
       (xmls:parse 
          (flex:octets-to-string
             (zip:zipfile-entry-contents entry)
             :external-format :utf-8 ;;;; add this !
              )))))
(in-package :cl-user) ; go back to cl-user to use xlsx with multilingual support
;;; for example
(xlsx:sheet-names "Your_Language.xlsx")

full workaround

(defun get-entry (name zip)
  ;; ECMA-376 Part 2 (Open Packaging Conventions), section 6.2.5
  ;; https://ecma-international.org/publications-and-standards/standards/ecma-376/
  ;; specifies that XML parts must be encoded in UTF-8 or UTF-16 only.
  ;; Therefore, we detect encoding based on BOM only.
  ;; - UTF-8 / UTF-8-BOM / UTF-16LE / UTF-16BE
  (flet ((decode (octets)
           (let* ((len (length octets))
                  ;; Determine encoding based on BOM (Byte Order Mark)
                  (enc
                   (cond
                     ;; UTF-8 BOM: EF BB BF
                     ((and (>= len 3)
                           (= #xEF (aref octets 0))
                           (= #xBB (aref octets 1))
                           (= #xBF (aref octets 2)))
                      :utf-8)
                     ;; UTF-16LE BOM: FF FE
                     ((and (>= len 2)
                           (= #xFF (aref octets 0))
                           (= #xFE (aref octets 1)))
                      :utf-16le)
                     ;; UTF-16BE BOM: FE FF
                     ((and (>= len 2)
                           (= #xFE (aref octets 0))
                           (= #xFF (aref octets 1)))
                      :utf-16be)
                     ;; Default to UTF-8 if no BOM is present
                     (t :utf-8)))
                  ;; Skip length: number of BOM bytes to exclude from string
                  ;; This ensures the returned string does not include the BOM character (U+FEFF)
                  (skip
                   (case enc
                     (:utf-8    (if (and (>= len 3)
                                         (= #xEF (aref octets 0))) 3 0))
                     (:utf-16le 2)
                     (:utf-16be 2)
                     (otherwise 0))))
             ;; Decode octets to string, skipping BOM
             (flex:octets-to-string (subseq octets skip)
                                    :external-format enc))))
    (let ((entry (zip:get-zipfile-entry name zip)))
      (when entry
        (xmls:parse (decode (zip:zipfile-entry-contents entry)))))))