Files
LithosAnanake/src/physics_pipelining_metrics.c
T
Robert Allan JamesandClaude Sonnet 5 1a2ec565e8 Zero C-compiler warnings on all three architectures; fix real restore_vm_state() bug
Maintainability sweep (prompted by "this is getting hard to maintain"):
fixed the remaining three warning classes after the missing-field-
initializers commit -- 2x -Wsign-compare (control_words.c, cast at the
comparison site rather than changing cf_last_mode's type, which
deliberately holds a -999 sentinel outside vm_mode_t's valid range),
2x -Wstringop-truncation (mkcapsule.c, strncpy+manual-null-terminate
replaced with the idiomatic snprintf equivalent), and 26x
-Wunused-parameter (mostly documented stubs, silenced with the repo's
existing (void)param; idiom).

One of the unused-parameter warnings was not a deliberate stub -- a
real bug. restore_vm_state() (test_common.c) is named, documented, and
called by nine real call sites (acl_words_test.c x8 plus its own
internal use) as "restore saved VM state", but ignored all four of its
parameters and hard-reset to a fixed baseline instead, silently not
restoring what any caller actually saved. Fixed to actually assign the
passed-in dsp/rsp/error/mode. Found while fixing warnings, reported
before touching it, fixed/tested/documented/committed on explicit
instruction.

Verified: all three architectures build with zero C-compiler warnings
(amd64: 3040 -> 0; aarch64's one remaining note is lld-link's own
unrelated linker warning, not a C warning). Full amd64 acceptance boot
post-fix: POST 1003/965/0/0/38 (total/passed/failed/errors/stubs),
"ALL IMPLEMENTED TESTS PASSED!", contract checks (A4'/A1) all passed,
dict_hash=0x24b4279f0670aa3a -- an exact match to this document's own
previously-recorded baseline hash.

.claude/CLAUDE.md corrected to describe the real -Wno-error= exemption
list instead of the "-Wall -Werror" oversimplification. FABRIC-2.md
Section J records the full sweep, including doc-tree staleness findings
flagged but not fixed this pass (docs/lithosananke/ROADMAP.md branch
topology, docs/03-architecture/word-acl/DESIGN.md's Phase 7 claim
contradicting CLAUDE.md, top-level ROADMAP.md's stale StarForth-era
status, the Isabelle pipeline-metrics model mismatch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 22:31:57 -04:00

755 lines
30 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.
*/
/*
*** StarForth ***
physics_pipelining_metrics.c - Word transition tracking implementation
Tracks word-to-word transitions to support speculative execution/pipelining.
Uses only fixed-point Q48.16 arithmetic (no floating-point).
*/
#include "physics_pipelining_metrics.h"
#include "vm.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* ============================================================================
* Q48.16 Fixed-Point Arithmetic Helpers
* ============================================================================
*/
/**
* @brief Convert a nanosecond count to Q48.16 fixed-point.
*
* Shifts @p ns left by 16 bits to produce a Q48.16 value where the integer
* part represents nanoseconds (1.0 ns = 65536 in Q48.16). Used to convert
* raw platform nanosecond timestamps into the common Q48.16 format before
* accumulating into @c prefetch_latency_saved_q48 and
* @c misprediction_cost_q48.
*
* @param ns Integer nanosecond value to convert.
* @return @p ns in Q48.16 format (@p ns × 65536).
*/
static inline int64_t ns_to_q48(int64_t ns) {
return ns << 16;
}
/**
* @brief Convert a Q48.16 fixed-point value to integer nanoseconds (truncated).
*
* Shifts @p q48 right by 16 bits, discarding the fractional part. The result
* is the integer nanosecond count represented by the Q48.16 value. Used in
* @c transition_metrics_stats_string() to convert the accumulated
* @c prefetch_latency_saved_q48 and @c misprediction_cost_q48 back to
* human-readable nanosecond integers for the stats report string.
*
* @param q48 Value in Q48.16 format.
* @return Integer nanosecond equivalent (truncated, not rounded).
*/
static inline int64_t q48_to_ns(int64_t q48) {
return q48 >> 16;
}
/**
* @brief Divide a Q48.16 value by a @c uint64_t, returning Q48.16.
*
* Computes @p a_q48 / @p b using integer division. The result preserves the
* Q48.16 scaling of @p a_q48 because @p b is dimensionless. Guards against
* divide-by-zero by returning 0 when @p b == 0. Used by
* @c transition_metrics_get_probability_q48() to compute the per-word
* transition probability:
* @code
* P(next = w) = (count_q48 << 16) / total_transitions
* @endcode
* where @c count_q48 = (uint64_t)count << 16 and @c total_transitions is the
* denominator @p b.
*
* @param a_q48 Numerator in Q48.16 format.
* @param b Denominator as a plain @c uint64_t (dimensionless count).
* @return @p a_q48 / @p b in Q48.16; 0 if @p b == 0.
*/
static inline int64_t q48_div_u64(int64_t a_q48, uint64_t b) {
if (b == 0) return 0;
return a_q48 / (int64_t)b;
}
/* ============================================================================
* Transition Metrics Implementation
* ============================================================================
*/
/**
* @brief Initialise a @c WordTransitionMetrics structure to its empty, ready-to-record state.
*
* Sets all counter and accumulator fields to zero and performs the following
* allocations:
*
* - @c metrics->transition_heat is left NULL — it is lazily allocated by the
* first call to @c transition_metrics_record() to avoid the @c dict_size
* parameter being required at construction time.
*
* - If @c TRANSITION_WINDOW_SIZE > 0, allocates @c metrics->context_window as
* a zero-filled array of @c TRANSITION_WINDOW_SIZE @c uint32_t word IDs and
* sets @c metrics->actual_window_size accordingly. If the allocation fails,
* @c actual_window_size is set to 0 and context tracking degrades gracefully
* (all context functions become no-ops).
*
* - @c metrics->context_transitions is left NULL — it is lazily allocated when
* Phase 2 context-pattern analysis is implemented.
*
* Returns 0 on success or 1 if @p metrics is NULL.
*
* @param metrics The @c WordTransitionMetrics to initialise; must not be NULL.
* @return 0 on success; 1 if @p metrics is NULL.
*/
int transition_metrics_init(WordTransitionMetrics *metrics) {
if (!metrics) return -1;
metrics->transition_heat = NULL; /* Lazy allocation on first use */
metrics->total_transitions = 0;
metrics->prefetch_attempts = 0;
metrics->prefetch_hits = 0;
metrics->prefetch_misses = 0;
metrics->prefetch_latency_saved_q48 = 0;
metrics->misprediction_cost_q48 = 0;
metrics->max_transition_probability_q48 = 0;
metrics->most_likely_next_word_id = 0;
/* Phase 1 Extension: Initialize context window (window size = 2 by default) */
if (TRANSITION_WINDOW_SIZE > 0) {
metrics->context_window = (uint32_t *)calloc(TRANSITION_WINDOW_SIZE, sizeof(uint32_t));
if (metrics->context_window) {
metrics->actual_window_size = TRANSITION_WINDOW_SIZE;
metrics->context_window_pos = 0;
} else {
metrics->actual_window_size = 0; /* Degraded: no context tracking */
}
} else {
metrics->actual_window_size = 0;
}
metrics->context_transitions = NULL; /* Lazy allocation on first context transition */
metrics->total_context_transitions = 0;
return 0;
}
/**
* @brief Record one word-to-word transition in the transition heat table.
*
* Called from the interpreter loop each time a word executes and there is a
* known previous word. Increments @c metrics->transition_heat[next_word_id]
* by one and @c metrics->total_transitions by one.
*
* **Lazy allocation:** The @c transition_heat array is not allocated at
* @c transition_metrics_init() time; on the first call when
* @c metrics->transition_heat == NULL, it is allocated as a zero-filled
* array of @p dict_size @c uint64_t counters. If the allocation fails 1 is
* returned and the transition is silently dropped.
*
* If @p next_word_id ≥ @p dict_size the call is rejected with 1 to prevent
* an out-of-bounds write into @c transition_heat (which was allocated with
* size @p dict_size).
*
* The accumulated @c transition_heat[] table is consumed by
* @c transition_metrics_get_probability_q48() and
* @c transition_metrics_update_cache() to determine the most-likely next word
* for speculative prefetch.
*
* @param metrics The metrics structure to update.
* @param next_word_id Zero-based word ID of the word that followed the
* current word; must be < @p dict_size.
* @param dict_size Current dictionary size, used both as the bounds
* check for @p next_word_id and as the allocation size
* for the lazy-allocated @c transition_heat array.
* @return 0 on success; 1 on NULL @p metrics, out-of-bounds
* @p next_word_id, or allocation failure.
*/
int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word_id, uint32_t dict_size) {
if (!metrics || next_word_id >= dict_size) {
return -1;
}
/* Lazy allocation: only allocate transition_heat array when first transition occurs */
if (!metrics->transition_heat) {
metrics->transition_heat = (uint64_t *)calloc(dict_size, sizeof(uint64_t));
if (!metrics->transition_heat) {
return -1; /* Memory allocation failure */
}
}
/* Record the transition */
metrics->transition_heat[next_word_id]++;
metrics->total_transitions++;
return 0;
}
/**
* @brief Return the transition probability to @p target_word_id in Q48.16 format.
*
* Computes P(next = @p target_word_id | current word) as a Q48.16 fixed-point
* ratio:
* @code
* P = (transition_heat[target_word_id] << 16) / total_transitions
* @endcode
* using @c q48_div_u64() to guard against divide-by-zero.
*
* Returns 0 if:
* - @p metrics is NULL.
* - @c metrics->total_transitions == 0 (no transitions recorded yet).
* - @c metrics->transition_heat is NULL (lazy allocation has not occurred).
*
* The caller is responsible for ensuring @p target_word_id is within the
* bounds of the allocated @c transition_heat array; this function does not
* re-check bounds after verifying the NULL guard above (bounds were enforced
* during @c transition_metrics_record()).
*
* Consumed by @c transition_metrics_update_cache() (scans all words to find
* the maximum) and @c transition_metrics_should_speculate() (checks whether
* a specific word's probability exceeds @c SPECULATION_THRESHOLD_Q48).
*
* @param metrics The metrics structure to query.
* @param target_word_id Zero-based word ID whose transition probability is
* requested; must be within the dict_size used at record time.
* @return P(next = @p target_word_id) in Q48.16; 0 if unavailable.
*/
int64_t transition_metrics_get_probability_q48(const WordTransitionMetrics *metrics, uint32_t target_word_id) {
if (!metrics || metrics->total_transitions == 0) {
return 0;
}
/* Safety: Bounds check on target_word_id to prevent out-of-bounds read
* Note: transition_heat is allocated with dict_size passed to transition_metrics_record()
* Callers must ensure target_word_id < dict_size used during allocation */
if (!metrics->transition_heat) {
return 0;
}
uint64_t count = metrics->transition_heat[target_word_id];
/* Probability = count / total_transitions, in Q48.16 format */
/* = (count << 16) / total_transitions */
int64_t count_q48 = (int64_t)count << 16;
return q48_div_u64(count_q48, metrics->total_transitions);
}
/**
* @brief Scan the transition heat table and cache the highest-probability next word.
*
* Iterates over all @p dict_size entries in @c metrics->transition_heat[],
* calling @c transition_metrics_get_probability_q48() for each non-zero entry
* and tracking the maximum. After the scan:
* - @c metrics->max_transition_probability_q48 holds the peak probability (Q48.16).
* - @c metrics->most_likely_next_word_id holds the word ID of that peak.
*
* These two cached fields allow @c transition_metrics_should_speculate() to
* make a prefetch decision in O(1) without re-scanning the full table on every
* word dispatch.
*
* If @p metrics, @c transition_heat, or @c total_transitions are not valid,
* both fields are reset to 0 and the function returns immediately.
*
* Called after each @c transition_metrics_record() in the high-frequency
* interpreter path, so the implementation must be fast. The full @p dict_size
* scan runs in O(dict_size) and skips zero-count entries early.
*
* @param metrics The metrics structure to update.
* @param dict_size Number of entries in @c metrics->transition_heat[];
* must match the size used at allocation time.
*/
void transition_metrics_update_cache(WordTransitionMetrics *metrics, uint32_t dict_size) {
if (!metrics || !metrics->transition_heat || metrics->total_transitions == 0) {
metrics->max_transition_probability_q48 = 0;
metrics->most_likely_next_word_id = 0;
return;
}
int64_t max_prob_q48 = 0;
uint32_t max_idx = 0;
/* Find word with highest transition count */
for (uint32_t i = 0; i < dict_size; i++) {
if (metrics->transition_heat[i] > 0) {
int64_t prob_q48 = transition_metrics_get_probability_q48(metrics, i);
if (prob_q48 > max_prob_q48) {
max_prob_q48 = prob_q48;
max_idx = i;
}
}
}
metrics->max_transition_probability_q48 = max_prob_q48;
metrics->most_likely_next_word_id = max_idx;
}
/**
* @brief Record a successful speculative prefetch and the latency it saved.
*
* Increments @c metrics->prefetch_hits by one and accumulates @p latency_saved_ns
* (converted to Q48.16 via @c ns_to_q48()) into
* @c metrics->prefetch_latency_saved_q48. The accumulated total is reported by
* @c transition_metrics_stats_string() as the aggregate latency benefit of
* correct speculation.
*
* Called by the speculative-execution path when a prefetched word matches the
* word that was actually executed next.
*
* @param metrics The metrics structure to update; no-op if NULL.
* @param latency_saved_ns The number of nanoseconds saved by avoiding a
* full dictionary lookup on this particular hit.
*/
void transition_metrics_record_prefetch_hit(WordTransitionMetrics *metrics, int64_t latency_saved_ns) {
if (!metrics) return;
metrics->prefetch_hits++;
metrics->prefetch_latency_saved_q48 += ns_to_q48(latency_saved_ns);
}
/**
* @brief Record a failed speculative prefetch and its recovery overhead.
*
* Increments @c metrics->prefetch_misses by one and accumulates
* @p recovery_cost_ns (converted to Q48.16 via @c ns_to_q48()) into
* @c metrics->misprediction_cost_q48. The accumulated total is the aggregate
* penalty paid for incorrect speculation and is reported by
* @c transition_metrics_stats_string() as "Misprediction Cost".
*
* The net benefit of the pipelining subsystem is:
* @code
* net_ns = prefetch_latency_saved_q48 - misprediction_cost_q48
* @endcode
* A positive net indicates that speculation is profitable overall.
*
* Called by the speculative-execution path when the prefetched word does not
* match the word that was actually executed next (branch misprediction analogue).
*
* @param metrics The metrics structure to update; no-op if NULL.
* @param recovery_cost_ns The number of nanoseconds spent unwinding the
* incorrect speculation (cache flush, state restore).
*/
void transition_metrics_record_prefetch_miss(WordTransitionMetrics *metrics, int64_t recovery_cost_ns) {
if (!metrics) return;
metrics->prefetch_misses++;
metrics->misprediction_cost_q48 += ns_to_q48(recovery_cost_ns);
}
/**
* @brief Decide whether to speculatively prefetch @p target_word_id.
*
* Applies a two-gate policy before authorising speculation:
*
* 1. **Minimum sample gate:** @c metrics->total_transitions must be ≥
* @c MIN_SAMPLES_FOR_SPECULATION. Below this threshold the transition
* probabilities are statistically unreliable — speculation would be
* indistinguishable from guessing.
*
* 2. **Probability threshold gate:** the Q48.16 probability returned by
* @c transition_metrics_get_probability_q48(metrics, @p target_word_id)
* must be ≥ @c SPECULATION_THRESHOLD_Q48. Words with very low conditional
* probability of following the current word are not worth prefetching.
*
* Both gates must pass for the function to return 1. A Phase 4 ROI check
* (comparing expected latency savings against expected misprediction cost) is
* planned but not yet implemented; a TODO comment marks the extension point.
*
* Returns 0 if @p metrics or @c transition_heat are NULL, or if either gate
* fails.
*
* @param metrics The metrics structure to consult.
* @param target_word_id Zero-based ID of the candidate next word.
* @return 1 if speculation is recommended; 0 otherwise.
*/
int transition_metrics_should_speculate(const WordTransitionMetrics *metrics, uint32_t target_word_id) {
if (!metrics || !metrics->transition_heat) {
return 0; /* No data yet */
}
/* Check: do we have enough samples? */
if (metrics->total_transitions < MIN_SAMPLES_FOR_SPECULATION) {
return 0; /* Not enough data */
}
/* Get probability of this word following */
int64_t prob_q48 = transition_metrics_get_probability_q48(metrics, target_word_id);
/* Check: does probability exceed threshold? */
if (prob_q48 < SPECULATION_THRESHOLD_Q48) {
return 0; /* Probability too low */
}
/* TODO: Phase 4 - Add ROI check here */
/* For now, threshold check is sufficient */
return 1; /* Should speculate */
}
/**
* @brief Allocate and return a human-readable transition metrics summary string.
*
* Allocates a 2048-byte buffer and fills it with a multi-line report:
* - Total transitions recorded.
* - Prefetch attempts, hits (with hit-rate %), and misses.
* - Aggregate latency saved by correct speculation (from
* @c prefetch_latency_saved_q48, converted to integer ns via @c q48_to_ns()).
* - Aggregate misprediction cost (from @c misprediction_cost_q48).
* - Net benefit (saved cost) in nanoseconds.
* - The most-likely next word ID and its raw Q48.16 probability.
*
* The caller is responsible for calling @c free() on the returned pointer.
* Returns NULL if @p metrics is NULL or @c malloc() fails.
*
* @param metrics The metrics structure to summarise.
* @return Heap-allocated NUL-terminated stats string; caller must @c free().
* NULL on allocation failure or NULL @p metrics.
*/
char *transition_metrics_stats_string(const WordTransitionMetrics *metrics) {
if (!metrics) return NULL;
/* Allocate buffer for stats string */
char *buf = (char *)malloc(2048);
if (!buf) return NULL;
int saved_ns = q48_to_ns(metrics->prefetch_latency_saved_q48);
int cost_ns = q48_to_ns(metrics->misprediction_cost_q48);
int net_ns = saved_ns - cost_ns;
/* Calculate hit rate */
uint64_t total_speculations = metrics->prefetch_attempts;
double hit_rate = (total_speculations > 0) ? (100.0 * metrics->prefetch_hits / total_speculations) : 0.0;
snprintf(buf, 2048,
"Transition Statistics:\n"
" Total Transitions: %lu\n"
" Prefetch Attempts: %lu\n"
" Prefetch Hits: %lu (%.1f%% hit rate)\n"
" Prefetch Misses: %lu\n"
" Latency Saved: %d ns\n"
" Misprediction Cost: %d ns\n"
" Net Benefit: %d ns\n"
" Most Likely Next: word[%u] (P=0x%lx Q48.16)\n",
metrics->total_transitions,
metrics->prefetch_attempts,
metrics->prefetch_hits, hit_rate,
metrics->prefetch_misses,
saved_ns,
cost_ns,
net_ns,
metrics->most_likely_next_word_id,
metrics->max_transition_probability_q48);
return buf;
}
/**
* @brief Reset a @c WordTransitionMetrics to empty without freeing the context window.
*
* Frees and NULLs @c metrics->transition_heat (so the next
* @c transition_metrics_record() will re-allocate it), then zeroes all
* counter and accumulator fields. Also zeroes the context-window array
* (if allocated) and resets @c context_window_pos to 0, and frees and NULLs
* @c context_transitions. @c actual_window_size and the @c context_window
* pointer itself are preserved — the window stays allocated and ready for
* reuse.
*
* Intended for DoE benchmark runs that need a clean statistical baseline
* between treatment cells without the overhead of full teardown and
* reinitialisation. Cheaper than @c transition_metrics_cleanup() +
* @c transition_metrics_init() because it avoids reallocating the context
* window.
*
* @param metrics The metrics structure to reset; no-op if NULL.
*/
void transition_metrics_reset(WordTransitionMetrics *metrics) {
if (!metrics) return;
if (metrics->transition_heat) {
free(metrics->transition_heat);
metrics->transition_heat = NULL;
}
metrics->total_transitions = 0;
metrics->prefetch_attempts = 0;
metrics->prefetch_hits = 0;
metrics->prefetch_misses = 0;
metrics->prefetch_latency_saved_q48 = 0;
metrics->misprediction_cost_q48 = 0;
metrics->max_transition_probability_q48 = 0;
metrics->most_likely_next_word_id = 0;
/* Phase 1 Extension: Reset context window */
if (metrics->context_window) {
memset(metrics->context_window, 0, metrics->actual_window_size * sizeof(uint32_t));
metrics->context_window_pos = 0;
}
if (metrics->context_transitions) {
free(metrics->context_transitions);
metrics->context_transitions = NULL;
}
metrics->total_context_transitions = 0;
}
/**
* @brief Release all heap memory held by a @c WordTransitionMetrics.
*
* Frees and NULLs:
* - @c metrics->transition_heat — the per-word transition count array.
* - @c metrics->context_window — the sliding context-window array.
* - @c metrics->context_transitions — the Phase 2 sparse hash table (if allocated).
*
* Does NOT zero the struct fields beyond NULLing the freed pointers. The
* caller must not use @p metrics after this call without a subsequent
* @c transition_metrics_init(). Contrast with @c transition_metrics_reset(),
* which keeps the context window allocated for reuse.
*
* No-op if @p metrics is NULL.
*
* @param metrics The metrics structure to clean up; no-op if NULL.
*/
void transition_metrics_cleanup(WordTransitionMetrics *metrics) {
if (!metrics) return;
if (metrics->transition_heat) {
free(metrics->transition_heat);
metrics->transition_heat = NULL;
}
/* Phase 1 Extension: Clean up context window */
if (metrics->context_window) {
free(metrics->context_window);
metrics->context_window = NULL;
}
if (metrics->context_transitions) {
free(metrics->context_transitions);
metrics->context_transitions = NULL;
}
}
/* ============================================================================
* Context-Aware Transition Functions (Phase 1 Extension)
* ============================================================================
*
* Phase 1: Collect data only (these functions are stubs)
* Phase 2: Implement analysis logic
* Phase 3: Use for adaptive decisions
*/
/**
* @brief Slide the context window forward by one word, appending @p word_id.
*
* Implements a fixed-length sliding window over the recent word-ID history.
* Each call shifts all existing entries one position to the left (dropping
* the oldest), then writes @p word_id into the last slot:
* @code
* context_window[0..n-2] = context_window[1..n-1]
* context_window[n-1] = word_id
* @endcode
* This produces a FIFO queue of the last @c actual_window_size word IDs
* that can be passed to @c transition_metrics_record_context() for
* context-sensitive transition tracking (Phase 2).
*
* Returns 1 and is a no-op if:
* - @p metrics is NULL.
* - @c context_window is NULL (allocation at init failed — degraded mode).
* - @c actual_window_size == 0.
*
* @param metrics The metrics structure whose context window is updated.
* @param word_id The word ID just executed; appended to the right of the window.
* @return 0 on success; 1 if the context window is unavailable.
*/
int transition_metrics_update_context_window(WordTransitionMetrics *metrics, uint32_t word_id) {
if (!metrics || !metrics->context_window || metrics->actual_window_size == 0) {
return -1; /* Context tracking not initialized */
}
/* Slide window forward: shift elements left, add new word on right */
for (uint32_t i = 0; i < metrics->actual_window_size - 1; i++) {
metrics->context_window[i] = metrics->context_window[i + 1];
}
/* Add new word at end of window */
metrics->context_window[metrics->actual_window_size - 1] = word_id;
return 0;
}
/**
* @brief Record a context-conditioned transition (Phase 1 collection stub).
*
* Accepts a @p context window of @p window_size preceding word IDs and the
* @p next_word_id that followed. In Phase 1 only the total count is tracked:
* @c metrics->total_context_transitions is incremented by one, providing a
* baseline count that Phase 2 analysis can use to size its hash tables.
*
* **Phase 2 TODO:** Build a sparse hash table keyed on the context pattern
* @c (context[0], …, context[window_size-1]) to record per-context next-word
* distributions. The Phase 2 analysis will feed
* @c transition_metrics_binary_chop_suggest_window() with accuracy data to
* tune the optimal window size.
*
* Returns 1 (no-op) if @p metrics or @p context are NULL, @p window_size is
* 0, or @p next_word_id ≥ @p dict_size.
*
* @param metrics The metrics structure to update.
* @param context Array of @p window_size word IDs preceding @p next_word_id.
* @param window_size Number of entries in @p context; must be > 0.
* @param next_word_id Word ID that followed the context sequence.
* @param dict_size Current dictionary size; used for bounds checking
* @p next_word_id.
* @return 0 on success; 1 on invalid arguments.
*/
int transition_metrics_record_context(WordTransitionMetrics *metrics,
const uint32_t *context,
uint32_t window_size,
uint32_t next_word_id,
uint32_t dict_size) {
if (!metrics || !context || window_size == 0 || next_word_id >= dict_size) {
return -1;
}
/* Phase 1: Just count total context transitions for validation */
metrics->total_context_transitions++;
/* TODO Phase 2: Build sparse hash table of context patterns
* For now, we collect data but don't analyze it.
* This is the foundation for Phase 2's binary chop tuning.
*/
return 0;
}
/**
* @brief Allocate and return a context-window status report string (Phase 1 stub).
*
* Allocates a 1024-byte buffer and fills it with a brief Phase 1 collection
* status message:
* - @c actual_window_size — the active window width (0 if allocation failed).
* - @c total_context_transitions — the number of context transitions recorded.
* - A "[Phase 1 Collection - Phase 2 Analysis Pending]" status line.
*
* Phase 2 will replace this stub with a real accuracy report showing per-window-size
* prediction accuracy derived from the sparse context-pattern hash table. The
* accuracy data will feed @c transition_metrics_binary_chop_suggest_window()
* to converge on the optimal context window width.
*
* The caller is responsible for calling @c free() on the returned pointer.
* Returns NULL if @p metrics is NULL or @c malloc() fails.
*
* @param metrics The metrics structure to report on.
* @return Heap-allocated NUL-terminated string; caller must @c free().
* NULL on allocation failure or NULL @p metrics.
*/
char *transition_metrics_context_accuracy_string(const WordTransitionMetrics *metrics) {
if (!metrics) return NULL;
char *buf = (char *)malloc(1024);
if (!buf) return NULL;
/* Phase 1: Stub - just report collection status */
snprintf(buf, 1024,
"Context Metrics Status:\n"
" Actual Window Size: %u\n"
" Total Context Transitions: %lu\n"
" Status: [Phase 1 Collection - Phase 2 Analysis Pending]\n",
metrics->actual_window_size,
metrics->total_context_transitions);
return buf;
}
/**
* @brief Suggest a new context window size using binary-chop search (Phase 1 stub).
*
* In Phase 2 this function will implement a binary-chop search over window sizes
* to converge on the width that maximises context-prediction accuracy:
* - Start at window = 2.
* - If accuracy improves when doubling, try 4; if it declines, try 1.
* - Continue halving/doubling until the accuracy delta falls below a threshold.
*
* **Phase 1 stub behaviour:** Doubles the current window each call up to 8,
* then wraps back to 1:
* - @p current_window == 1 → suggests 2
* - @p current_window == 2 → suggests 4
* - @p current_window == 4 → suggests 8
* - @p current_window ≥ 8 → suggests 1 (cycle back)
* - Any other value → returns @p current_window unchanged.
*
* The @p accuracy_at_current parameter (prediction accuracy [0.0, 1.0] at the
* current window size) is accepted for API stability with the Phase 2 caller
* but is not used in the stub.
*
* @param metrics The metrics structure (unused in Phase 1 stub;
* reserved for Phase 2 accuracy history).
* @param current_window Currently active context window size.
* @param accuracy_at_current Prediction accuracy at @p current_window
* (unused in Phase 1 stub; range [0.0, 1.0]).
* @return Suggested new window size.
*/
uint32_t transition_metrics_binary_chop_suggest_window(const WordTransitionMetrics *metrics,
uint32_t current_window,
double accuracy_at_current) {
(void)metrics;
(void)accuracy_at_current;
/* Phase 1: Stub - just return doubled window size per user's request
* Phase 2 will implement actual binary chop search:
* - Start at window=2
* - If accuracy improves, try doubling (2→4)
* - If accuracy declines, try halving (4→2)
* - Converge to optimal size
*/
if (current_window >= 8) return 1; /* Maximum: back to 1 */
if (current_window == 1) return 2; /* 1 → 2 */
if (current_window == 2) return 4; /* 2 → 4 (doubling) */
if (current_window == 4) return 8; /* 4 → 8 (doubling) */
return current_window; /* Fallback */
}