Files
LithosAnanake/include/q48_16.h
T
Robert Allan James cb4326c712 starkernel: item 4.3.3b -- geometry drawing wordset, fixed Q.TO-INT sign bug
Adds LINE (Bresenham in raster space, endpoints projected once each --
valid because the cavalier projection is linear), CIRCLE/ELLIPSE
(36-segment polygon approximation), and ARC (18 segments over a caller
radian range) to capsules/fabric.4th (blocks 4903-4912). TO-RASTER
factored out of CART-PLOT (same behavior) so LINE can reuse the
projection+flip for both endpoints.

Found mid-implementation: colon definitions cannot span block boundaries
in this capsule loader -- verified with a throwaway test capsule, the
continuation lands in a [CAPSULE][DEFER] path that never resolves. LINE's
body is split across LINE-SETUP/LINE-DONE?/LINE-STUCK?/LINE-STEP, each
self-contained within its block, rather than one long definition.

A fourth real bug, serious this time: CIRCLE's first live test rendered
only one quadrant, then hung the VM for several minutes on a follow-up
call. Root cause: q48_to_u64() (include/q48_16.h and
include/starkernel/q48_16.h, backing Q.TO-INT) did an unsigned logical
shift, corrupting any negative Q48.16 value into a huge garbage integer
instead of sign-extending -- inevitable once Q.SIN/Q.COS leave the first
quadrant. That garbage became a bogus LINE target with no bound on
LINE-STEP's Bresenham loop. Fixed q48_to_u64 to shift through a signed
int64_t intermediate (bit-identical for the non-negative case). Also added
LINE-STUCK? (LSTEPS vs FB-WIDTH+FB-HEIGHT, the true worst case for an
on-screen line) as a defense-in-depth cap against any future bad target.

Verified live on amd64 after both fixes: -65536 Q.TO-INT . now prints -1;
LINE/CIRCLE/ARC/ELLIPSE all complete without hanging or erroring, and a
combined screendump shows all four rendering correctly and distinctly.

All three architectures boot clean to ok> with the DoE completing;
dict_hash identical across all three and unchanged from 4.3.3a (expected
-- fabric.4th isn't loaded at boot, and the Q.TO-INT fix doesn't change
dictionary structure).

FABRIC.md item 4.3.3b marked done with full acceptance evidence.
2026-08-07 15:20:58 -04:00

305 lines
9.3 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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) 20232025 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.
*/
#ifndef Q48_16_H
#define Q48_16_H
#include <stdint.h>
/* ============================================================================
* Q48.16 Fixed-Point Arithmetic
* ============================================================================
*
* Format: uint64_t with fixed decimal point after bit 15
* - Bits 0-15: Fractional part (1/65536 resolution)
* - Bits 16-63: Integer part (up to 2^48-1)
*
* Example: 0x00010000 = 1.0, 0x00018000 = 1.5, 0x00010001 = 1 + 1/65536
*
* All operations are integer-only: no floating-point, no transcendental functions.
* Used for inference engine calculations (variance, regression, diagnostics).
*
* Why Q48.16?
* - 48-bit integer range: 0 to 281,474,976,710,655 (enough for VM metrics)
* - 16-bit fractional: ~0.0000152 resolution (precise enough for statistical calculations)
* - Perfect fit for 64-bit uint64_t: efficient on modern CPUs
*/
typedef uint64_t q48_16_t;
/* ============================================================================
* Core Arithmetic Operations (All Q48.16 ↔ Q48.16)
* ============================================================================
*/
/*
* @brief Multiply two Q48.16 values, keeping result in Q48.16
*
* Math: (a / 2^16) * (b / 2^16) * 2^16 = (a * b) / 2^16
* Returns: (a * b) >> 16
*
* @param a First Q48.16 operand
* @param b Second Q48.16 operand
* @return (a * b) >> 16 in Q48.16 format
*/
q48_16_t q48_mul(q48_16_t a, q48_16_t b);
/*
* @brief Divide two Q48.16 values, keeping result in Q48.16
*
* Math: (a / 2^16) / (b / 2^16) * 2^16 = (a << 16) / b
* Returns: (a << 16) / b
*
* @param a Dividend in Q48.16
* @param b Divisor in Q48.16 (non-zero)
* @return (a << 16) / b in Q48.16 format
*/
q48_16_t q48_div(q48_16_t a, q48_16_t b);
/* Guard: skip static inlines already provided by include/starkernel/q48_16.h
* (both headers share the same definitions; STARKERNEL_Q48_16_H is defined
* by the kernel version so we avoid -Werror redefinition in mixed builds). */
#ifndef STARKERNEL_Q48_16_H
/**
* @brief Add two Q48.16 values
*
* Math: (a / 2^16) + (b / 2^16) = (a + b) / 2^16
* Returns: a + b (no shift needed, both have same scaling)
*
* @param a First Q48.16 operand
* @param b Second Q48.16 operand
* @return a + b in Q48.16 format
*/
static inline q48_16_t q48_add(q48_16_t a, q48_16_t b) {
return a + b;
}
/**
* @brief Subtract two Q48.16 values
*
* Math: (a / 2^16) - (b / 2^16) = (a - b) / 2^16
* Returns: a - b (no shift needed)
*
* @param a Minuend in Q48.16
* @param b Subtrahend in Q48.16
* @return a - b in Q48.16 format
*/
static inline q48_16_t q48_sub(q48_16_t a, q48_16_t b) {
return a - b;
}
/**
* @brief Absolute value of Q48.16
*
* @param a Q48.16 value
* @return |a| in Q48.16 format
*/
static inline q48_16_t q48_abs(q48_16_t a) {
return (a < 0x8000000000000000ULL) ? a : (0ULL - a);
}
/* ============================================================================
* Conversion Operations (u64 ↔ Q48.16)
* ============================================================================
*/
/**
* @brief Convert unsigned 64-bit integer to Q48.16
*
* Math: u * 2^16 (shift left by 16 bits)
* Example: q48_from_u64(1) = 0x10000 (1.0 in Q48.16)
*
* @param u Unsigned 64-bit integer (max 2^48-1 to avoid overflow)
* @return u << 16 in Q48.16 format
*/
static inline q48_16_t q48_from_u64(uint64_t u) {
return u << 16;
}
/**
* @brief Convert Q48.16 to a 64-bit integer (truncate fractional)
*
* Math: q / 2^16 (arithmetic shift right by 16 bits)
* Example: q48_to_u64(0x10000) = 1, q48_to_u64(-0x10000) = -1
*
* q48_16_t values are two's-complement signed under the hood (q48_neg/
* q48_abs already treat them that way); a plain unsigned (logical) shift
* would corrupt negative inputs instead of sign-extending them, so this
* shifts via a signed intermediate. Bit-identical to the old behavior for
* non-negative q (the only case this function's other caller, the
* inference engine's sum-of-squares accumulation, ever produces).
*
* @param q Q48.16 value
* @return q >> 16, sign-extended, reinterpreted as uint64_t
*/
static inline uint64_t q48_to_u64(q48_16_t q) {
return (uint64_t)(((int64_t)q) >> 16);
}
#endif /* STARKERNEL_Q48_16_H */
/*
* @brief Convert double to Q48.16 (for testing/initialization only)
*
* NOT used in inference engine (inference is integer-only).
* Provided for testing and diagnostic tools only.
*
* @param d Double-precision float
* @return d * 2^16 as Q48.16
*/
q48_16_t q48_from_double(double d);
/*
* @brief Convert Q48.16 to double (for testing/diagnostics only)
*
* NOT used in inference engine (inference is integer-only).
* Provided for logging and RStudio dashboard output.
*
* @param q Q48.16 value
* @return q / 2^16 as double
*/
double q48_to_double(q48_16_t q);
/* ============================================================================
* Approximation Operations (Integer-Only)
* ============================================================================
*/
/*
* @brief Approximate natural logarithm in Q48.16 (integer-only)
*
* Purpose: Required for decay slope inference via exponential fitting
* Model: ln(heat[t]) = ln(h0) - slope*t
*
* Method: Piecewise linear approximation or Newton-Raphson iteration
* - Input x must be > 0 (assertion check)
* - Output is ln(x) in Q48.16 format
* - Precision: ~5% error acceptable (tuning parameter)
* - No floating-point operations
*
* Implementation Strategy:
* 1. Use bit position (floor(log2(x))) as coarse estimate
* 2. Refine with Newton iteration (3-4 cycles for Q48.16 precision)
* 3. Keep all arithmetic in integer space
*
* Example: q48_log_approx(65536) = log(1.0) in Q48.16 ≈ 0
* q48_log_approx(131072) = log(2.0) in Q48.16 ≈ 0x0000B172
*
* @param x Value to take logarithm of (x > 0)
* @return ln(x) in Q48.16 format
*/
q48_16_t q48_log_approx(uint64_t x);
/*
* @brief Approximate exponential e^x in Q48.16 (integer-only, future use)
*
* Purpose: Might be needed for inverse operations or model validation
* Status: Defined but not required for Phase 1 inference engine
* Method: Similar to q48_log_approx (piecewise + Newton)
*
* @param q Q48.16 exponent
* @return e^q in Q48.16 format
*/
q48_16_t q48_exp_approx(q48_16_t q);
/*
* @brief Approximate square root in Q48.16 (integer-only)
*
* Purpose: Compute R² (fit quality) for regression diagnostics
* Method: Newton-Raphson for integer-only sqrt
*
* @param q Q48.16 value
* @return sqrt(q) in Q48.16 format
*/
q48_16_t q48_sqrt_approx(q48_16_t q);
/*
* @brief Approximate sin(q) in Q48.16 (integer-only, Taylor series)
*
* Purpose: Console drawing fabric's CIRCLE/ARC/ELLIPSE (FABRIC.md item 4.3.3b)
* Method: Range-reduce into [-pi, pi], then Taylor series
*
* @param q Angle in Q48.16 format (radians, any magnitude)
* @return sin(q) in Q48.16 format
*/
q48_16_t q48_sin_approx(q48_16_t q);
/*
* @brief Approximate cos(q) in Q48.16 (integer-only, Taylor series)
*
* Purpose: Console drawing fabric's CIRCLE/ARC/ELLIPSE (FABRIC.md item 4.3.3b)
* Method: Range-reduce into [-pi, pi], then Taylor series
*
* @param q Angle in Q48.16 format (radians, any magnitude)
* @return cos(q) in Q48.16 format
*/
q48_16_t q48_cos_approx(q48_16_t q);
/* ============================================================================
* Diagnostic / Testing Utilities
* ============================================================================
*/
/*
* @brief Pretty-print Q48.16 value to string for debugging
*
* Format: "integer.fractional" (e.g., "1.50000")
* Uses internal static buffer - NOT thread-safe
*
* @param q Q48.16 value
* @return Pointer to static string (valid until next call)
*/
const char* q48_to_string(q48_16_t q);
/*
* @brief Validate Q48.16 value is within safe range
*
* Checks: Does not overflow on mul/div operations
* Returns: 1 if valid, 0 if risky
*
* @param q Q48.16 value
* @return 1 if safe, 0 if risky
*/
int q48_is_valid(q48_16_t q);
#endif /* Q48_16_H */