Addresses items #1 (partial) and #2 of docs/working/archive/session-logs/2026-07-24-punch-list.md. docs/lithosananke/hosted-acceptance-test/README.md: - riscv64 leg used riscv64-linux-gnu-gcc, which fails to build this tree (nanosleep visibility under -std=c99). Replaced with the working clang-18 --target=riscv64-linux-gnu --sysroot=/usr/riscv64-linux-gnu invocation, verified end-to-end. - All three arch sections referenced a -c "<script>" flag that has never existed in cli.c/main.c. Corrected to the working `echo "..." | starforth -s` pattern, verified on all three architectures. - Updated Prerequisites: qemu-user alone is sufficient (guest binaries are static; qemu-user-static provides static *emulators*, not required here). Source fixes (ported from the old pre-split monorepo's master, commit 4db9946a, where they were made but never carried over to this line): - src/math_portable.c: `-100LL << 16` is UB (shifting a negative value) under clang's -Wshift-negative-value; changed to `-(100LL << 16)`. - src/physics_pipelining_metrics.c: removed dead q48_mul_q48() (-Wunused-function under clang; GCC doesn't flag this by default). - src/word_source/editor_words.c: removed dead set_scr() (same reason). These three were required just to get the documented clang build to compile at all. The punch list's higher-severity item — a genuine SIGSEGV-causing register-reuse hazard in vm_pop_asm/vm_rpop_asm (include/vm_asm_opt_riscv64.h) — is intentionally NOT included here; it's a separate, more careful change and isn't required for this build to succeed (latent only under clang; this repo's kernel build uses GCC). Verified: all three hosted builds compile and run correctly (amd64 native, aarch64 via qemu-aarch64, riscv64 via qemu-riscv64), each printing "3 Goodbye!" for the piped `1 2 + . BYE` script. All three Makefile.starkernel builds (amd64/aarch64/riscv64) still compile cleanly with these shared vendored-source changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
264 lines
9.2 KiB
C
264 lines
9.2 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
*/
|
||
|
||
/*
|
||
* math_portable.c
|
||
*
|
||
* Pure 64-bit fixed-point Q48.16 math functions.
|
||
* Suitable for embedded environments with no libm or floating-point
|
||
* (HISTORICAL: also L4Re, through mid-2026; removed as an active target).
|
||
* All arithmetic is integer-only for maximum production performance.
|
||
*
|
||
* Q48.16 Format:
|
||
* - 48-bit signed integer + 16-bit fractional part
|
||
* - 1.0 = 65536 (2^16)
|
||
* - Precision: 2^-16 ≈ 0.0000153
|
||
* - Range: ±140 trillion
|
||
*/
|
||
|
||
#include <stdint.h>
|
||
#include "math_portable.h"
|
||
|
||
/**
|
||
* @brief Compute exp(@p x) via Taylor series in Q48.16 fixed-point.
|
||
*
|
||
* Evaluates the classic Taylor expansion:
|
||
* @code
|
||
* exp(x) = 1 + x + x²/2! + x³/3! + … (up to 40 terms)
|
||
* @endcode
|
||
* entirely in 64-bit integer arithmetic using the Q48.16 representation
|
||
* (1.0 = 65536 = 2^16).
|
||
*
|
||
* The function is intentionally scoped to the range the physics engine
|
||
* actually uses — the argument to @c erf_q48() is @c -x² times a ratio
|
||
* near 1, yielding values in roughly the @c [-20, 0] interval. Outside
|
||
* this range the implementation provides coarse-grained clamping:
|
||
* - @p x == 0 → returns @c Q48 (1.0 exactly)
|
||
* - @p x > 20.0 → saturates at @c Q48 << 8 (approx e^20, avoids overflow)
|
||
* - @p x < -100.0 → underflows to 0 (result is negligible)
|
||
*
|
||
* The term loop exits early on the first term smaller than 1 ULP (the
|
||
* series has converged) and guards against intermediate overflow by
|
||
* clamping @c result at @c INT64_MAX-1.
|
||
*
|
||
* @note This is a file-local (@c static) helper; callers are
|
||
* @c erf_q48() only.
|
||
*
|
||
* @param x Input value in Q48.16 format (1.0 = 65536). Intended range:
|
||
* approximately [-100×65536, +20×65536].
|
||
* @return exp(@p x) in Q48.16 format; 0 on underflow; saturated at
|
||
* @c INT64_MAX-1 on overflow.
|
||
*/
|
||
static int64_t exp_q48(int64_t x) {
|
||
const int64_t Q48 = 65536LL; /* 2^16 */
|
||
|
||
/* Handle extremes */
|
||
if (x == 0) return Q48;
|
||
if (x > (20LL << 16)) return Q48 << 8; /* Approximate e^20 */
|
||
if (x < -(100LL << 16)) return 0; /* Underflow to zero */
|
||
|
||
/* Taylor series: exp(x) = Σ x^n / n! (in fixed-point) */
|
||
int64_t result = Q48; /* Start with 1 */
|
||
int64_t term = Q48;
|
||
int64_t x_power = x; /* x^1 */
|
||
|
||
for (int i = 1; i < 40; i++) {
|
||
/* term = x^i / i! */
|
||
term = (x_power / (int64_t)i) / Q48; /* Scale down to avoid overflow */
|
||
if (term > -1 && term < 1) break; /* Negligible */
|
||
|
||
result += term;
|
||
x_power = (x_power * x) / Q48; /* Keep x^n scaled */
|
||
|
||
/* Guard against overflow */
|
||
if (result < 0 && result < (1LL << 62)) break;
|
||
if (result > 0 && result > (1LL << 62)) {
|
||
result = 9223372036854775806LL; /* Saturate at INT64_MAX-1 */
|
||
break;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* @brief Compute sqrt(@p x) via Newton-Raphson in Q48.16 fixed-point.
|
||
*
|
||
* Applies the Newton-Raphson recurrence:
|
||
* @code
|
||
* r_{n+1} = (r_n² + x) / (2 × r_n)
|
||
* @endcode
|
||
* for up to 20 iterations, with all arithmetic scaled to the Q48.16
|
||
* representation (1.0 = 65536). Intermediate products use 8-bit right
|
||
* shifts on both operands before squaring to prevent 64-bit overflow:
|
||
* @code
|
||
* guess_squared = (guess >> 8) * (guess >> 8)
|
||
* @endcode
|
||
* This sacrifices 16 bits of precision in the squared term, which is
|
||
* acceptable because the division by @c 2*guess restores the correct
|
||
* order of magnitude. Convergence is declared when successive iterates
|
||
* differ by at most 1 in Q48.16 units (sub-nanosecond for timing uses).
|
||
*
|
||
* Special cases handled without iteration:
|
||
* - @p x ≤ 0 → returns 0 (no imaginary results)
|
||
* - @p x == 65536 (1.0) → returns 65536 exactly
|
||
*
|
||
* Initial guess: @c x/2 for @p x > 1.0, else @c 1.0. Values above 1.0
|
||
* converge from above; values in (0, 1.0) converge from the 1.0 initial
|
||
* guess.
|
||
*
|
||
* @param x Non-negative value in Q48.16 format (1.0 = 65536).
|
||
* Negative values are treated as zero.
|
||
* @return sqrt(@p x) in Q48.16 format; 0 for @p x ≤ 0.
|
||
*/
|
||
int64_t sqrt_q48(int64_t x) {
|
||
const int64_t Q48 = 65536LL;
|
||
|
||
/* Handle special cases */
|
||
if (x < 0) return 0;
|
||
if (x == 0) return 0;
|
||
if (x == Q48) return Q48; /* sqrt(1.0) = 1.0 */
|
||
|
||
/* Initial guess: average of x and 1 */
|
||
int64_t guess = (x > Q48) ? (x >> 1) : Q48;
|
||
|
||
/* Newton-Raphson iterations */
|
||
for (int i = 0; i < 20; i++) {
|
||
/* next = (guess^2 + x) / (2 * guess)
|
||
* In Q48.16: (guess^2) >> 16 + x, then divide by 2*guess
|
||
*/
|
||
int64_t guess_squared = (guess >> 8) * (guess >> 8); /* Avoid overflow */
|
||
int64_t next = (guess_squared + (x >> 16)) / (2 * guess);
|
||
|
||
/* Check convergence */
|
||
int64_t diff = next - guess;
|
||
if (diff > -2 && diff < 2) return next;
|
||
|
||
guess = next;
|
||
}
|
||
|
||
return guess;
|
||
}
|
||
|
||
/**
|
||
* @brief Compute erf(@p x) via Abramowitz & Stegun 7.1.26 in Q48.16 fixed-point.
|
||
*
|
||
* Implements the rational approximation from Abramowitz & Stegun, formula 7.1.26:
|
||
* @code
|
||
* erf(x) ≈ sign(x) × sqrt(1 − exp(−x² × (4/π + a·x²) / (1 + a·x²)))
|
||
* @endcode
|
||
* where @c a ≈ 0.147 (in Q48.16: 9633). The maximum error of the true
|
||
* mathematical approximation is @c |ε| < 1.5×10⁻⁷; the fixed-point
|
||
* encoding introduces additional rounding of approximately 1 ULP in Q48.16.
|
||
*
|
||
* All arithmetic is performed in 64-bit integer Q48.16 format — no
|
||
* floating-point operations. The computation sequence:
|
||
* 1. Extract sign; work with @c |x|.
|
||
* 2. Compute @c x² with 8-bit prescaling to prevent overflow:
|
||
* @c x_sq = (x >> 8) * (x >> 8).
|
||
* 3. Evaluate the A&S numerator @c (4/π + a·x²) and denominator @c (1 + a·x²).
|
||
* 4. Compute @c ratio = numerator / denominator (shift to preserve Q48.16 scale).
|
||
* 5. Compute @c exp_arg = −x² × ratio and evaluate @c exp_q48(exp_arg).
|
||
* 6. Compute @c base = 1 − exp_val, clamp to [0, 1].
|
||
* 7. Return @c sign × sqrt_q48(base).
|
||
*
|
||
* Q48.16 constants used:
|
||
* - @c a_q48 = 9633 (0.147 × 65536)
|
||
* - @c four_over_pi_q48 = 83328 (4/π × 65536 ≈ 1.2732 × 65536)
|
||
*
|
||
* Consumed by @c hotwords_speedup_estimate() to compute P(speedup > 1.1×)
|
||
* and P(speedup > 2.0×) via the normal CDF identity:
|
||
* @code
|
||
* P(Z ≤ z) = 0.5 × (1 + erf(z / √2))
|
||
* @endcode
|
||
* where @c √2 in Q48.16 = 92681.
|
||
*
|
||
* @param x Input value in Q48.16 format (dimensionless Z-score;
|
||
* 1.0 = 65536). May be negative; sign determines the sign
|
||
* of the result.
|
||
* @return erf(@p x) in Q48.16 format, in the range [−65536, +65536]
|
||
* corresponding to (−1.0, +1.0).
|
||
*/
|
||
int64_t erf_q48(int64_t x) {
|
||
const int64_t Q48 = 65536LL;
|
||
|
||
if (x == 0) return 0;
|
||
|
||
/* Compute sign and work with absolute value */
|
||
int sign = (x < 0) ? -1 : 1;
|
||
if (x < 0) x = -x;
|
||
|
||
/* Abramowitz & Stegun coefficient a ≈ 0.147 (in Q48.16) */
|
||
const int64_t a_q48 = 9633LL; /* 0.147 * 65536 */
|
||
const int64_t four_over_pi_q48 = 83328LL; /* 4/π * 65536 */
|
||
|
||
/* Compute x^2 in Q48.16 */
|
||
int64_t x_sq = (x >> 8) * (x >> 8); /* Scaled to avoid overflow */
|
||
|
||
/* Compute denominator: 1 + a*x^2 */
|
||
int64_t denom = Q48 + ((a_q48 * x_sq) >> 16);
|
||
if (denom == 0) denom = 1; /* Guard divide-by-zero */
|
||
|
||
/* Compute numerator: 4/π + a*x^2 */
|
||
int64_t numer = four_over_pi_q48 + ((a_q48 * x_sq) >> 16);
|
||
|
||
/* Compute ratio = numerator / denominator */
|
||
int64_t ratio = (numer << 16) / denom;
|
||
|
||
/* Compute exp argument: -x^2 * ratio */
|
||
int64_t exp_arg = -(x_sq * ratio / Q48);
|
||
|
||
/* Compute exp(-x^2 * ratio) */
|
||
int64_t exp_val = exp_q48(exp_arg);
|
||
|
||
/* Compute base: 1 - exp(exp_arg) */
|
||
int64_t base = Q48 - exp_val;
|
||
if (base < 0) base = 0;
|
||
if (base > Q48) base = Q48;
|
||
|
||
/* Compute sqrt(base) */
|
||
int64_t sqrt_base = sqrt_q48(base);
|
||
|
||
/* Apply sign and return */
|
||
return sign * sqrt_base;
|
||
}
|