388 lines
15 KiB
C
388 lines
15 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.
|
||
|
||
*/
|
||
|
||
/*
|
||
*** StarForth ***
|
||
|
||
physics_metadata.c - Phase 1 physics metadata helpers
|
||
|
||
Captures thermodynamic-style signals for dictionary entries without
|
||
introducing heavy runtime cost. Future phases can build on these helpers
|
||
to drive physics-aware scheduling and storage placement.
|
||
*/
|
||
|
||
#include "../include/physics_metadata.h"
|
||
#include "../include/ssm_jacquard.h"
|
||
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
|
||
/* L8 FINAL INTEGRATION: L1 heat tracking is always-on (internal physics signal) */
|
||
|
||
/**
|
||
* @brief Clamp a uint64_t value to the uint32_t range.
|
||
*
|
||
* @param value Input value to clamp
|
||
* @return @c UINT32_MAX if @c value > UINT32_MAX, else @c (uint32_t)value
|
||
*/
|
||
static uint32_t clamp_u32(uint64_t value) {
|
||
return (value > UINT32_MAX) ? UINT32_MAX : (uint32_t) value;
|
||
}
|
||
|
||
/**
|
||
* @brief Derive a smoothed Q8 temperature from raw execution heat.
|
||
*
|
||
* Converts @c execution_heat to Q8 by left-shifting 8 bits, clamps to
|
||
* UINT16_MAX, then applies a 1/4 EMA blend with @c prior_q8 to suppress
|
||
* jitter: result = (3*prior + target) / 4. Returns 0 for non-positive heat.
|
||
*
|
||
* @param execution_heat Raw execution heat counter for the word
|
||
* @param prior_q8 Previous smoothed temperature in Q8 format
|
||
* @return Updated smoothed temperature in Q8 format
|
||
*/
|
||
static uint16_t temperature_from_execution_heat(cell_t execution_heat, uint16_t prior_q8) {
|
||
if (execution_heat <= 0) return 0;
|
||
uint64_t scaled = ((uint64_t) execution_heat) << 8; /* convert to Q8 */
|
||
uint16_t target = (scaled > UINT16_MAX) ? UINT16_MAX : (uint16_t) scaled;
|
||
/* Smooth with a simple EMA to avoid jitter */
|
||
return (uint16_t)((3u * prior_q8 + target) / 4u);
|
||
}
|
||
|
||
/**
|
||
* @brief Derive the physics state flags bitmask from a DictEntry's word flags.
|
||
*
|
||
* Maps @c WORD_IMMEDIATE → @c PHYSICS_STATE_IMMEDIATE,
|
||
* @c WORD_PINNED → @c PHYSICS_STATE_PINNED,
|
||
* @c WORD_HIDDEN → @c PHYSICS_STATE_HIDDEN, and
|
||
* @c WORD_COMPILED → @c PHYSICS_STATE_COMPILED.
|
||
*
|
||
* @param entry DictEntry to inspect (NULL returns 0)
|
||
* @return Bitmask of @c PHYSICS_STATE_* flags
|
||
*/
|
||
static uint8_t derive_state_flags(const DictEntry *entry) {
|
||
if (!entry) return 0;
|
||
uint8_t flags = 0;
|
||
if (entry->flags & WORD_IMMEDIATE) flags |= PHYSICS_STATE_IMMEDIATE;
|
||
if (entry->flags & WORD_PINNED) flags |= PHYSICS_STATE_PINNED;
|
||
if (entry->flags & WORD_HIDDEN) flags |= PHYSICS_STATE_HIDDEN;
|
||
if (entry->flags & WORD_COMPILED) flags |= PHYSICS_STATE_COMPILED;
|
||
return flags;
|
||
}
|
||
|
||
/**
|
||
* @brief Initialise physics metadata for a newly registered DictEntry.
|
||
*
|
||
* Zeroes all @c DictPhysics fields, sets @c mass_bytes to @c header_bytes
|
||
* (the byte size of the entry header), and derives initial @c state_flags
|
||
* from the entry's word flags. Called from @c register_word() immediately
|
||
* after the entry is linked into the dictionary.
|
||
*
|
||
* @param entry DictEntry to initialise
|
||
* @param header_bytes Byte size of the entry's header region (for mass)
|
||
*/
|
||
void physics_metadata_init(DictEntry *entry, uint32_t header_bytes) {
|
||
if (!entry) return;
|
||
entry->physics.temperature_q8 = 0;
|
||
entry->physics.last_active_ns = 0;
|
||
entry->physics.last_decay_ns = 0;
|
||
entry->physics.last_decay_tick = 0;
|
||
entry->physics.mass_bytes = header_bytes;
|
||
entry->physics.avg_latency_ns = 0;
|
||
entry->physics.state_flags = derive_state_flags(entry);
|
||
entry->physics.acl_hint = 0;
|
||
entry->physics.pubsub_mask = 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Update the mass (byte footprint) of a DictEntry's physics metadata.
|
||
*
|
||
* Called after a colon definition or CREATE body is fully compiled so the
|
||
* mass reflects the total compiled size, not just the header.
|
||
*
|
||
* @param entry DictEntry to update
|
||
* @param total_bytes Total compiled byte size (header + body)
|
||
*/
|
||
void physics_metadata_set_mass(DictEntry *entry, uint32_t total_bytes) {
|
||
if (!entry) return;
|
||
entry->physics.mass_bytes = total_bytes;
|
||
}
|
||
|
||
/* ============================================================================
|
||
* INTENT: Update word's physics metadata after execution
|
||
* FL1: Heat accumulation feedback loop - converts raw execution_heat counter
|
||
* to smoothed temperature_q8 metric and updates activity timestamps
|
||
* WHY: Atomic heat increment (FL1) happens in execution loop; this function
|
||
* derives secondary metrics (temperature, timestamps) for observability
|
||
* ============================================================================ */
|
||
/**
|
||
* @brief Update physics metadata after a word execution completes.
|
||
*
|
||
* Derives and stores a new smoothed @c temperature_q8 from @c execution_heat,
|
||
* and updates both @c last_active_ns and @c last_decay_ns to @c now_ns.
|
||
* Called by @c physics_post_execute() on the post-execution path.
|
||
*
|
||
* @param entry DictEntry that was just executed
|
||
* @param execution_heat Current raw execution heat counter
|
||
* @param now_ns Current monotonic timestamp in nanoseconds
|
||
*/
|
||
void physics_metadata_touch(DictEntry *entry, cell_t execution_heat, uint64_t now_ns) {
|
||
if (!entry) return;
|
||
entry->physics.temperature_q8 = temperature_from_execution_heat(execution_heat, entry->physics.temperature_q8);
|
||
entry->physics.last_active_ns = now_ns;
|
||
entry->physics.last_decay_ns = now_ns;
|
||
}
|
||
|
||
/**
|
||
* @brief Re-derive and update @c state_flags for a DictEntry.
|
||
*
|
||
* Called after word flags change (e.g. IMMEDIATE is set during compilation)
|
||
* to keep the physics state flags in sync with the word flags.
|
||
*
|
||
* @param entry DictEntry whose state flags are to be refreshed
|
||
*/
|
||
void physics_metadata_refresh_state(DictEntry *entry) {
|
||
if (!entry) return;
|
||
entry->physics.state_flags = derive_state_flags(entry);
|
||
}
|
||
|
||
/**
|
||
* @brief Record an execution latency sample into the per-word EMA.
|
||
*
|
||
* Clamps @c sample_ns to uint32_t, then blends it into @c avg_latency_ns
|
||
* with a 1/4 EMA: new = (3*prior + sample) / 4. Seeds @c avg_latency_ns
|
||
* directly from the first sample.
|
||
*
|
||
* @param entry DictEntry whose latency EMA is updated
|
||
* @param sample_ns Measured execution duration in nanoseconds
|
||
*/
|
||
void physics_metadata_record_latency(DictEntry *entry, uint64_t sample_ns) {
|
||
if (!entry) return;
|
||
uint32_t sample = clamp_u32(sample_ns);
|
||
uint32_t prior = entry->physics.avg_latency_ns;
|
||
if (prior == 0) {
|
||
entry->physics.avg_latency_ns = sample;
|
||
} else {
|
||
entry->physics.avg_latency_ns = (uint32_t)((3ull * prior + sample) / 4ull);
|
||
}
|
||
}
|
||
|
||
typedef struct physics_seed_definition {
|
||
const char *name;
|
||
uint16_t temperature_q8;
|
||
uint32_t avg_latency_ns;
|
||
uint8_t acl_hint;
|
||
uint8_t pubsub_mask;
|
||
} physics_seed_definition_t;
|
||
|
||
static const physics_seed_definition_t physics_seed_table[] = {
|
||
{"IF", 0x0100u, 400u, 0, 0},
|
||
{"ELSE", 0x0100u, 400u, 0, 0},
|
||
{"THEN", 0x0100u, 350u, 0, 0},
|
||
{"BEGIN", 0x0120u, 450u, 0, 0},
|
||
{"WHILE", 0x0120u, 450u, 0, 0},
|
||
{"REPEAT", 0x0120u, 450u, 0, 0},
|
||
{"DO", 0x0180u, 500u, 0, 0},
|
||
{"LOOP", 0x0180u, 500u, 0, 0},
|
||
{"+LOOP", 0x0200u, 520u, 0, 0},
|
||
{"LEAVE", 0x0200u, 520u, 0, 0},
|
||
{".", 0x0080u, 300u, 0, 0},
|
||
{"EMIT", 0x0180u, 800u, 0, 0x02u},
|
||
{"TYPE", 0x0180u, 900u, 0, 0x02u},
|
||
{"BLOCK", 0x0200u, 950u, 0, 0x04u},
|
||
{"BUFFER", 0x01C0u, 900u, 0, 0x04u},
|
||
{"SAVE-BUFFERS", 0x0260u, 1400u, 0, 0x04u},
|
||
{"FLUSH", 0x0240u, 1200u, 0, 0x04u},
|
||
{"LOAD", 0x0220u, 1100u, 0, 0x04u},
|
||
{"LIST", 0x0200u, 1000u, 0, 0x04u},
|
||
{"SAVE-SYSTEM", 0x0280u, 2000u, 0x80u, 0x08u},
|
||
{NULL, 0, 0, 0, 0}
|
||
};
|
||
|
||
/**
|
||
* @brief Apply DoE-derived prior physics values to a named DictEntry.
|
||
*
|
||
* Searches the static @c physics_seed_table for an entry matching
|
||
* @c entry->name. If found, seeds @c temperature_q8, @c avg_latency_ns,
|
||
* @c acl_hint, and @c pubsub_mask with values derived from the 2^7
|
||
* factorial DoE analysis. Only non-zero seed fields overwrite the current
|
||
* value. Called from @c register_word() for built-in primitives.
|
||
*
|
||
* @param entry DictEntry to seed (no-op if NULL or no matching table entry)
|
||
*/
|
||
void physics_metadata_apply_seed(DictEntry *entry) {
|
||
if (!entry || !entry->name_len) return;
|
||
for (const physics_seed_definition_t *seed = physics_seed_table; seed->name; ++seed) {
|
||
if ((int) strlen(seed->name) != entry->name_len) continue;
|
||
if (strncmp(seed->name, entry->name, entry->name_len) != 0) continue;
|
||
if (seed->temperature_q8) entry->physics.temperature_q8 = seed->temperature_q8;
|
||
if (seed->avg_latency_ns) entry->physics.avg_latency_ns = seed->avg_latency_ns;
|
||
if (seed->acl_hint) entry->physics.acl_hint = seed->acl_hint;
|
||
if (seed->pubsub_mask) entry->physics.pubsub_mask = seed->pubsub_mask;
|
||
break;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* ============================================================================
|
||
* Phase 2: Linear Decay Mechanism
|
||
* ============================================================================
|
||
*
|
||
* Reduces execution_heat over time to model:
|
||
* - OS context switches (old task's heat becomes stale)
|
||
* - Temporal locality (recent access patterns > distant past)
|
||
* - Dissipation (physical analogy: thermal equilibrium)
|
||
*
|
||
* Mathematical Model (Discrete Linear):
|
||
* H(t) = max(0, H_0 - d*t)
|
||
*
|
||
* Where:
|
||
* H(t) = heat at time t
|
||
* H_0 = initial heat
|
||
* d = decay rate (heat units per heartbeat tick)
|
||
* t = heartbeat ticks elapsed since last execution -- never wall-clock
|
||
* time; see physics_metadata_apply_linear_decay()'s doc comment
|
||
*
|
||
* Frozen words (WORD_FROZEN flag) are exempt from decay.
|
||
* ============================================================================
|
||
*/
|
||
|
||
/**
|
||
* @brief Apply Loop #3 linear heat decay to a DictEntry.
|
||
*
|
||
* Implements the thermodynamic dissipation model: H(t) = max(0, H₀ - d*t),
|
||
* where t is measured in heartbeat ticks (@c vm->heartbeat.tick_count),
|
||
* never in wall-clock time. The decay amount is computed as
|
||
* (elapsed_ticks * slope_q48) >> 16 using the Q48.16 adaptive decay slope
|
||
* stored in the VM's heartbeat state. Uses a GCC CAS loop for atomic heat
|
||
* update when @c __GNUC__ is defined, falling back to a plain subtraction
|
||
* otherwise.
|
||
*
|
||
* Ticks, not nanoseconds: this entry's @c execution_heat feeds directly
|
||
* into @c dict_hash (see @c capsule_dict_hash_hook()), so any wall-clock
|
||
* dependency here makes dictionary state -- and therefore dict_hash --
|
||
* architecture-variable under QEMU/TCG's differing real emulation speed,
|
||
* or whenever real elapsed time between two touches of the same entry
|
||
* varies for reasons unrelated to execution (e.g. a blocking I/O wait
|
||
* between them). @c vm->heartbeat.tick_count only advances from
|
||
* @c vm_tick(), called synchronously from the interpreter's own dispatch
|
||
* path -- a purely execution-count-driven clock, immune to how long any
|
||
* intervening real-time work (including blocking I/O) actually took.
|
||
*
|
||
* No-op when:
|
||
* - @c WORD_FROZEN flag is set on the entry
|
||
* - @c elapsed_ticks == 0 (no heartbeat tick has fired since last decay)
|
||
* - @c vm->ssm_config has L3 disabled by the Jacquard mode selector
|
||
*
|
||
* @param entry DictEntry whose heat is decayed
|
||
* @param elapsed_ticks Heartbeat ticks elapsed since last decay application
|
||
* @param vm VM providing the adaptive decay slope and L3 gate flag
|
||
*/
|
||
void physics_metadata_apply_linear_decay(DictEntry *entry, uint64_t elapsed_ticks, VM *vm) {
|
||
if (!entry || !vm) {
|
||
return;
|
||
}
|
||
|
||
/* Frozen words do not decay */
|
||
if (entry->flags & WORD_FROZEN) {
|
||
return;
|
||
}
|
||
|
||
/* Don't bother when no tick has fired since the last application */
|
||
if (elapsed_ticks == 0) {
|
||
return;
|
||
}
|
||
|
||
/* L8 GATE: Check if L3 (linear decay) is enabled by Jacquard mode selector */
|
||
if (vm->ssm_config) {
|
||
ssm_config_t *config = (ssm_config_t*)vm->ssm_config;
|
||
if (!config->L3_linear_decay) {
|
||
return; /* L3 disabled by L8 mode selector */
|
||
}
|
||
}
|
||
|
||
/* ========================================================================
|
||
* INTENT: Calculate decay amount using adaptive Q48.16 fixed-point math
|
||
* FL1: Heat decay - counterbalances FL1 accumulation to prevent runaway
|
||
* FL3: Adaptive slope tuning - inference engine adjusts decay_slope_q48
|
||
* based on statistical analysis of heat trajectory effectiveness
|
||
* WHY: decay_slope_q48 is expressed as heat units per tick, not per
|
||
* microsecond -- the inference engine (Loop #6) adaptively
|
||
* calibrates it against whatever units it's actually applied in,
|
||
* so no conversion constant is needed here, just a consistent unit.
|
||
* ======================================================================== */
|
||
uint64_t slope_q48 = physics_decay_slope_load(vm);
|
||
uint64_t decay_amount_raw = (elapsed_ticks * slope_q48) >> 16;
|
||
|
||
/* Clamp to cell_t range (avoid overflow) */
|
||
cell_t decay_amount = (decay_amount_raw > (uint64_t)0x7FFFFFFFFFFFFFFF)
|
||
? 0x7FFFFFFFFFFFFFFF
|
||
: (cell_t)decay_amount_raw;
|
||
|
||
if (decay_amount == 0) {
|
||
return;
|
||
}
|
||
|
||
#if defined(__GNUC__)
|
||
cell_t old_heat;
|
||
cell_t new_heat;
|
||
do {
|
||
old_heat = __atomic_load_n(&entry->execution_heat, __ATOMIC_RELAXED);
|
||
if (old_heat == 0) {
|
||
return;
|
||
}
|
||
new_heat = (decay_amount >= old_heat) ? 0 : (old_heat - decay_amount);
|
||
} while (!__atomic_compare_exchange_n(&entry->execution_heat,
|
||
&old_heat,
|
||
new_heat,
|
||
0,
|
||
__ATOMIC_RELAXED,
|
||
__ATOMIC_RELAXED));
|
||
#else
|
||
if (decay_amount >= entry->execution_heat) {
|
||
entry->execution_heat = 0;
|
||
} else {
|
||
entry->execution_heat -= decay_amount;
|
||
}
|
||
#endif
|
||
}
|