feat(rtl): migrate blaise_utf8 .s to inline asm — RTL now pure Pascal

This commit is contained in:
Graeme Geldenhuys 2026-06-25 12:54:18 +01:00
parent 52fd1d5b3b
commit 0c4a22f5e2
3 changed files with 162 additions and 249 deletions

View file

@ -5,19 +5,14 @@
# Licensed under the Apache License v2.0 with Runtime Library Exception.
# See LICENSE file in the project root for full license terms.
#
# Each Pascal unit is compiled directly via Blaise's unit-as-top-level
# mode (project_unit_as_toplevel, 2026-05-24): a single
# `blaise --source X.pas --output X.o` invocation runs qbe + cc -c
# + objcopy-embed in one step. No build-driver shims, no IR-strip
# sed pipelines, no intermediate .ssa/.s files needed.
# Each RTL unit is compiled directly via Blaise's unit-as-top-level mode: a
# single `blaise --source X.pas --output X.o` invocation produces the object.
# The RTL is now entirely pure Pascal (the four hand-written *_x86_64.s files
# were migrated to inline-asm `.pas` units), so the whole archive builds through
# Blaise's own internal assembler — no external assembler, no gcc, no .s files.
SHELL = /bin/bash
CC = gcc
CFLAGS = -O2 -Wall -Wextra -std=c11
SRC_DIR = src/main/c
PAS_SRC_DIR = src/main/pascal
ASM_DIR = src/main/asm
OBJ_DIR = target
LIB = $(OBJ_DIR)/blaise_rtl.a
@ -36,18 +31,16 @@ BLAISE = $(COMPILER_BIN)/blaise
#
# --assembler internal: assemble each unit to its .o with Blaise's own in-process
# assembler instead of `cc -c`, so building the RTL needs no external assembler.
# (The hand-written .s files in $(ASM_DIR) still go through $(CC) until Blaise can
# assemble a standalone .s; the Pascal units no longer touch gcc.)
BLAISE_FLAGS = --no-incremental --assembler internal
# Assembly sources — platform-specific, no C compiler needed.
# (blaise_atomic, blaise_setjmp, blaise_start migrated to inline-asm units.)
ASM_OBJS = $(OBJ_DIR)/blaise_utf8_x86_64.o
# Pascal RTL units compiled via the Blaise compiler.
# Every RTL unit is now pure Pascal (the four hand-written *_x86_64.s files —
# start, setjmp, atomic, utf8 — were migrated to inline-asm `.pas` units). The
# RTL therefore builds entirely through Blaise's own pipeline: no $(CC), no
# external assembler, no .s files.
PAS_OBJS = $(OBJ_DIR)/blaise_start.o \
$(OBJ_DIR)/blaise_atomic.o \
$(OBJ_DIR)/blaise_setjmp.o \
$(OBJ_DIR)/blaise_utf8.o \
$(OBJ_DIR)/blaise_mem.o \
$(OBJ_DIR)/blaise_str.o \
$(OBJ_DIR)/blaise_set.o \
@ -59,7 +52,7 @@ PAS_OBJS = $(OBJ_DIR)/blaise_start.o \
$(OBJ_DIR)/rtl_platform_layout_linux.o \
$(OBJ_DIR)/rtl_platform_posix.o
OBJS = $(ASM_OBJS) $(PAS_OBJS)
OBJS = $(PAS_OBJS)
.PHONY: all install clean
@ -68,12 +61,11 @@ all: $(LIB)
$(LIB): $(OBJS)
ar rcs $@ $^
# --- Assembly rules ---
$(OBJ_DIR)/%.o: $(ASM_DIR)/%.s
@mkdir -p $(OBJ_DIR)
$(CC) -c -o $@ $<
# --- Pascal unit rules (unit-as-top-level) ---
$(OBJ_DIR)/blaise_utf8.o: $(PAS_SRC_DIR)/blaise_utf8.pas
@mkdir -p $(OBJ_DIR)
$(BLAISE) $(BLAISE_FLAGS) --source $< --unit-path $(PAS_SRC_DIR) --output $@
$(OBJ_DIR)/blaise_start.o: $(PAS_SRC_DIR)/blaise_start.pas
@mkdir -p $(OBJ_DIR)
$(BLAISE) $(BLAISE_FLAGS) --source $< --unit-path $(PAS_SRC_DIR) --output $@

View file

@ -1,226 +0,0 @@
#
# Blaise An Object Pascal Compiler
# Copyright (c) 2026 Graeme Geldenhuys
# SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
# Licensed under the Apache License v2.0 with Runtime Library Exception.
# See LICENSE file in the project root for full license terms.
#
# SIMD-accelerated UTF-8 codepoint counting (x86_64).
#
# _Utf8CountCodePoints counts the number of UTF-8 codepoints in a byte buffer
# by counting non-continuation bytes (bytes where (b & 0xC0) != 0x80).
#
# Strategy: use AVX2 (32 bytes/iteration) when available, fall back to SSE2
# (16 bytes/iteration), then scalar for tail bytes.
#
# Calling convention (System V AMD64):
# %rdi = pointer to first byte of string data
# %esi = byte length of the string
# Returns codepoint count in %eax.
#
.text
.globl _Utf8CountCodePoints
.type _Utf8CountCodePoints, @function
_Utf8CountCodePoints:
# Fast path: empty or very short strings
testl %esi, %esi
jle .Lreturn_zero
movl %esi, %ecx # ecx = remaining bytes
xorl %eax, %eax # eax = codepoint count accumulator
xorl %edx, %edx # edx = current offset
# Check for AVX2 support via CPUID (cached in .Lavx2_flag)
cmpl $0, .Lavx2_checked(%rip)
jne .Lavx2_known
call .Lcheck_avx2
xorl %eax, %eax # restore accumulator (cpuid clobbers eax)
.Lavx2_known:
cmpl $0, .Lavx2_flag(%rip)
je .Lsse2_entry
# --- AVX2 path: 32 bytes per iteration ---
cmpl $32, %ecx
jb .Lsse2_entry
# 0x80 broadcast into ymm1 (continuation byte marker)
vpcmpeqb %ymm2, %ymm2, %ymm2 # ymm2 = all 0xFF
vpsrlw $1, %ymm2, %ymm2 # ymm2 = 0x7F7F...
vpaddw %ymm2, %ymm2, %ymm1 # This doesn't give 0x80 correctly
# Correct approach: use vpbroadcastb-equivalent via stack
# Actually: 0x80 = set top bit of each byte
vpcmpeqb %ymm2, %ymm2, %ymm2 # ymm2 = all 0xFF
vpsllw $7, %ymm2, %ymm1 # ymm1 = 0x80 in each byte position
# That gives 0x80,0x00 pattern in words. Need per-byte.
# Use vpabsb on all-ones to get 0x01, then shift.
# Simplest: build 0xC0 and 0x80 masks on stack.
# Better: count bytes where (byte & 0xC0) != 0x80
# Equivalent: count bytes where byte < 0x80 OR byte >= 0xC0
# Which is: NOT a continuation byte (10xxxxxx pattern)
# A continuation byte has bits 7:6 == 10.
# (byte + 0x40) sets bit 7 for 0x80..0xBF range overflow into 0xC0..0xFF
# Then test bit 7: if set = NOT continuation.
# Actually simplest SIMD approach: byte >= 0xC0 OR byte < 0x80
# = saturating subtract 0x80, then compare > 0x3F (catches >= 0xC0)
# OR original byte < 0x80
# Even simpler: (byte & 0xC0) != 0x80
# XOR with 0x80 (byte ^ 0x80) & 0xC0 != 0 iff not continuation
# shift right by 6 and sum? No.
#
# Standard trick: count bytes that are NOT 10xxxxxx.
# A byte is a lead/ASCII byte iff its top two bits are NOT 10.
# pcmpgtb is signed comparison, so:
# (signed) byte > -65 (0xBF as signed = -65) means byte >= 0xC0
# (signed) byte > -1 means byte >= 0 means byte < 0x80 (ASCII)... no.
#
# Classic UTF-8 SIMD count:
# For each byte B: is_not_continuation = ((B & 0xC0) != 0x80)
# Using signed arithmetic:
# Treat bytes as signed: continuation bytes 0x80..0xBF = -128..-65
# if (signed_byte > -65) NOT continuation (includes 0xC0..0xFF and 0x00..0x7F)
# pcmpgtb ymm_data, ymm_threshold(-65) 0xFF for non-continuation bytes
#
# Wait pcmpgtb(a, b) = a > b? No: pcmpgtb dest, src means dest[i] = (dest[i] > src[i]) ? 0xFF : 0x00
# We want: data[i] > -65 is_not_continuation
# So we need pcmpgtb with data in dest and -65 broadcast in src.
# But pcmpgtb is destructive. So:
# 1. Load data into ymm0
# 2. Set ymm1 = broadcast(-65) = 0xBF bytes
# 3. vpcmpgtb ymm2, ymm0, ymm1 ymm2[i] = 0xFF if data[i] > -65 (signed)
# 4. vpsrlw $7 on result... no, we need to count the 0xFF bytes.
# 5. Sum: vpsubb zeros, ymm2 (subtracting 0xFF = adding 1 per set byte)
# Actually -(-1) = 1, so vpsubb(0, mask) counts ones.
# But vpsubb has no immediate form; use pabsb on the result (abs(-1)=1, abs(0)=0)
# vpsabsb ymm2, ymm2 each byte is 0 or 1
# 6. vpsadbw to horizontally sum bytes 4 x 16-bit sums in qword lanes
# Build threshold: broadcast -65 (0xBF as signed) into ymm1
# -65 = 0xBF. All bytes 0xBF.
vpcmpeqb %ymm1, %ymm1, %ymm1 # ymm1 = all 0xFF
# We need 0xBF = ~0x40. Start from 0xFF, add 0xC0? No.
# 0xBF = 0xFF - 0x40. Use vpaddb with -0x40? We don't have that.
# Simplest: load from memory constant.
vmovdqa .Lthreshold_avx(%rip), %ymm1
vpxor %ymm3, %ymm3, %ymm3 # ymm3 = zero (accumulator)
.Lavx2_loop:
vmovdqu (%rdi, %rdx), %ymm0 # load 32 bytes
vpcmpgtb %ymm1, %ymm0, %ymm2 # ymm2[i] = 0xFF if data[i] > -65 (signed)
vpabsb %ymm2, %ymm2 # ymm2[i] = 0 or 1
vpaddb %ymm2, %ymm3, %ymm3 # accumulate byte counts (safe for 255 iters)
addl $32, %edx
subl $32, %ecx
cmpl $32, %ecx
jge .Lavx2_loop
# Horizontal sum of ymm3: vpsadbw sum each 8-byte lane into 64-bit
vpxor %ymm4, %ymm4, %ymm4
vpsadbw %ymm4, %ymm3, %ymm3 # ymm3 = 4 x 64-bit partial sums
# Extract and sum the 4 quadwords
vextracti128 $1, %ymm3, %xmm4
vpaddq %xmm4, %xmm3, %xmm3 # xmm3 = 2 x 64-bit sums
vpshufd $0x0E, %xmm3, %xmm4 # move high qword to low
vpaddq %xmm4, %xmm3, %xmm3
vmovd %xmm3, %r8d
addl %r8d, %eax
vzeroupper
jmp .Lsse2_entry
# --- SSE2 path: 16 bytes per iteration ---
.Lsse2_entry:
cmpl $16, %ecx
jb .Lscalar_entry
# Build threshold -65 (0xBF) in xmm1
movdqa .Lthreshold_sse(%rip), %xmm1
pxor %xmm3, %xmm3 # xmm3 = zero accumulator
.Lsse2_loop:
movdqu (%rdi, %rdx), %xmm0 # load 16 bytes
movdqa %xmm0, %xmm2 # copy (pcmpgtb is destructive)
pcmpgtb %xmm1, %xmm2 # xmm2[i] = 0xFF if data[i] > -65
pabsb %xmm2, %xmm2 # xmm2[i] = 0 or 1
paddb %xmm2, %xmm3 # accumulate (safe for 255 iters)
addl $16, %edx
subl $16, %ecx
cmpl $16, %ecx
jge .Lsse2_loop
# Horizontal sum of xmm3
pxor %xmm4, %xmm4
psadbw %xmm4, %xmm3 # xmm3 = 2 x 64-bit sums
movd %xmm3, %r8d
pshufd $0x0E, %xmm3, %xmm3
movd %xmm3, %r9d
addl %r8d, %eax
addl %r9d, %eax
# --- Scalar tail: remaining < 16 bytes ---
.Lscalar_entry:
testl %ecx, %ecx
jle .Lreturn
.Lscalar_loop:
movzbl (%rdi, %rdx), %r8d # load one byte
# Non-continuation if (byte & 0xC0) != 0x80
movl %r8d, %r9d
andl $0xC0, %r9d
cmpl $0x80, %r9d
setne %r9b
movzbl %r9b, %r9d
addl %r9d, %eax
incl %edx
decl %ecx
jnz .Lscalar_loop
.Lreturn:
ret
.Lreturn_zero:
xorl %eax, %eax
ret
# --- AVX2 detection subroutine ---
.Lcheck_avx2:
pushq %rbx
pushq %rcx
pushq %rdx
# CPUID leaf 7, subleaf 0 bit 5 of EBX = AVX2
movl $7, %eax
xorl %ecx, %ecx
cpuid
testl $(1 << 5), %ebx
setne %al
movzbl %al, %eax
movl %eax, .Lavx2_flag(%rip)
movl $1, .Lavx2_checked(%rip)
popq %rdx
popq %rcx
popq %rbx
ret
.size _Utf8CountCodePoints, .-_Utf8CountCodePoints
# --- Read-only data (threshold constant) ---
.section .rodata
.balign 32
.Lthreshold_avx:
.fill 32, 1, 0xBF
.balign 16
.Lthreshold_sse:
.fill 16, 1, 0xBF
# --- Mutable data (AVX2 detection cache) ---
.section .data
.Lavx2_checked:
.long 0
.Lavx2_flag:
.long 0

View file

@ -0,0 +1,147 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit blaise_utf8;
// UTF-8 code-point counter SIMD-accelerated (AVX2 with an SSE2 fallback and
// a scalar tail), with a one-time CPUID probe for AVX2.
//
// Inline-assembler port of runtime/src/main/asm/blaise_utf8_x86_64.s the body
// (and its threshold/flag data) move into an `asm end` routine so the RTL
// builds with Blaise's own internal assembler and needs no hand-written .s
// (docs/inline-asm-design.adoc, §"Migration of the .s files"). This was the
// last .s file; with it gone the RTL is pure Pascal + inline asm.
//
// Counts UTF-8 code points by counting bytes that are NOT continuation bytes
// (a byte is a continuation byte iff its top two bits are 10, i.e. signed value
// in [-128,-65]); the SIMD paths compare against the threshold -65 (0xBF).
//
// System V AMD64: Data -> %rdi, Len -> %esi; result (count) in %eax.
interface
function _Utf8CountCodePoints(Data: Pointer; Len: Integer): Integer;
implementation
function _Utf8CountCodePoints(Data: Pointer; Len: Integer): Integer;
assembler; nostackframe;
asm
testl %esi, %esi
jle .Lreturn_zero
movl %esi, %ecx
xorl %eax, %eax
xorl %edx, %edx
cmpl $0, .Lavx2_checked(%rip)
jne .Lavx2_known
call .Lcheck_avx2
xorl %eax, %eax
.Lavx2_known:
cmpl $0, .Lavx2_flag(%rip)
je .Lsse2_entry
cmpl $32, %ecx
jb .Lsse2_entry
vpcmpeqb %ymm2, %ymm2, %ymm2
vpsrlw $1, %ymm2, %ymm2
vpaddw %ymm2, %ymm2, %ymm1
vpcmpeqb %ymm2, %ymm2, %ymm2
vpsllw $7, %ymm2, %ymm1
vpcmpeqb %ymm1, %ymm1, %ymm1
vmovdqa .Lthreshold_avx(%rip), %ymm1
vpxor %ymm3, %ymm3, %ymm3
.Lavx2_loop:
vmovdqu (%rdi, %rdx), %ymm0
vpcmpgtb %ymm1, %ymm0, %ymm2
vpabsb %ymm2, %ymm2
vpaddb %ymm2, %ymm3, %ymm3
addl $32, %edx
subl $32, %ecx
cmpl $32, %ecx
jge .Lavx2_loop
vpxor %ymm4, %ymm4, %ymm4
vpsadbw %ymm4, %ymm3, %ymm3
vextracti128 $1, %ymm3, %xmm4
vpaddq %xmm4, %xmm3, %xmm3
vpshufd $0x0E, %xmm3, %xmm4
vpaddq %xmm4, %xmm3, %xmm3
vmovd %xmm3, %r8d
addl %r8d, %eax
vzeroupper
jmp .Lsse2_entry
.Lsse2_entry:
cmpl $16, %ecx
jb .Lscalar_entry
movdqa .Lthreshold_sse(%rip), %xmm1
pxor %xmm3, %xmm3
.Lsse2_loop:
movdqu (%rdi, %rdx), %xmm0
movdqa %xmm0, %xmm2
pcmpgtb %xmm1, %xmm2
pabsb %xmm2, %xmm2
paddb %xmm2, %xmm3
addl $16, %edx
subl $16, %ecx
cmpl $16, %ecx
jge .Lsse2_loop
pxor %xmm4, %xmm4
psadbw %xmm4, %xmm3
movd %xmm3, %r8d
pshufd $0x0E, %xmm3, %xmm3
movd %xmm3, %r9d
addl %r8d, %eax
addl %r9d, %eax
.Lscalar_entry:
testl %ecx, %ecx
jle .Lreturn
.Lscalar_loop:
movzbl (%rdi, %rdx), %r8d
movl %r8d, %r9d
andl $0xC0, %r9d
cmpl $0x80, %r9d
setne %r9b
movzbl %r9b, %r9d
addl %r9d, %eax
incl %edx
decl %ecx
jnz .Lscalar_loop
.Lreturn:
ret
.Lreturn_zero:
xorl %eax, %eax
ret
.Lcheck_avx2:
pushq %rbx
pushq %rcx
pushq %rdx
movl $7, %eax
xorl %ecx, %ecx
cpuid
testl $0x20, %ebx
setne %al
movzbl %al, %eax
movl %eax, .Lavx2_flag(%rip)
movl $1, .Lavx2_checked(%rip)
popq %rdx
popq %rcx
popq %rbx
ret
.section .rodata
.balign 32
.Lthreshold_avx:
.fill 32, 1, 0xBF
.balign 16
.Lthreshold_sse:
.fill 16, 1, 0xBF
.section .data
.Lavx2_checked:
.long 0
.Lavx2_flag:
.long 0
end;
end.