Stage MC-0c: shared VM code made per-VM or atomic (FABRIC-3.10.8 §3.4)
- Dictionary lookup index is per VM (vm->fc_index, struct SfFcIndex): was one global index rebuilt whenever a different VM was searched, and heat reorganization could reorder another VM's buckets. Allocated on first lookup, freed by vm_cleanup(); allocation failure counted/logged, lookup falls back to a newest-first dictionary walk. - PRNG: still one stream, stepped by compare-and-swap; SEED one store. - state_variable removed (write-only; STATE is vm->mode). - Sweep additions (Captain Bob's ruling): heartbeat_export.c delta last-values moved into each VM's HeartbeatState (were statics shared by every VM -- wrong across VMs today); TOP-WORDS list heap-sized per call (was static [1000]); inference_outputs_to_string() takes a caller buffer; three failure counters atomic; crc64 table published with release/acquire. - Confirmed, unchanged: g_runtime compiled out of the kernel, g_hotwords_cache unreferenced, ttf cache core-0-only. - Probe (removed): every VM has its own index; two VMs searched alternately 2x5000 lookups: 0 rebuilds, 0 wrong-VM results, all three ISAs. - Standard sequence on amd64/aarch64/riscv64: dict_hash set unchanged, G byte-identical, MINT/WIREBIND/USE ok, conservation true, 0 idle growth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
33f4090d6d
commit
355909d46f
@@ -262,3 +262,60 @@ Every lock balances individually on every ISA, at both readings. 0 recursions me
|
||||
- The G string is byte-identical to MC-0b and MC-0a on all three ISAs.
|
||||
- The `dict_hash` set and the ledger line are unchanged.
|
||||
- No stop condition was hit. **MC-0b2 done.**
|
||||
|
||||
### 3.4 MC-0c — shared VM code made per-VM or atomic (2026-09-25)
|
||||
|
||||
Scope: §1's second table, plus five further items found by a sweep of every file-scope and function-local mutable variable in the vendored VM sources the kernel compiles. Captain Bob ruled all of it into this stage (2026-09-25).
|
||||
|
||||
**§1's table:**
|
||||
|
||||
| Item | Finding | Change |
|
||||
|---|---|---|
|
||||
| dictionary lookup index (`sf_fc_list/count/cap`, `sf_cached_latest`) | One index for the whole system, holding whichever VM's dictionary was searched last: every switch between VMs that look words up rebuilt it. `dict_reorganize_buckets_by_heat()` reordered that shared index, which could be another VM's. | Per VM: `struct SfFcIndex` (`dictionary_heat_optimization.h`), `vm->fc_index`, allocated on first lookup and freed by both `vm_cleanup()`s. An allocation failure is counted and logged, and the lookup walks the dictionary newest-first instead (still correct). The heat-aware lookup and the heat reorganization use the VM's own index. The index's unused global reset is gone, as are the benchmark word's stale externs. The 256 buckets are one per possible first byte (the whole domain, not a capacity). |
|
||||
| `g_prng_state` (`starforth_words.c`) | One stream for all VMs, as accepted at Stage H; plain read-modify-write. | Still one stream. The step is a compare-and-swap loop, and SEED is a single atomic store. |
|
||||
| `g_runtime` (`physics_runtime.c`) | The kernel build (`STARFORTH_MINIMAL`) compiles stubs; `g_runtime` exists only hosted. | None. |
|
||||
| `state_variable` (`dictionary_manipulation_words.c`) | Written by INTERPRET, never read. STATE is `vm->mode`, already per VM. | Removed (dead). |
|
||||
| `g_hotwords_cache` (`physics_hotwords_cache.c`) | Defined, never referenced; the live cache is per VM (`vm->hotwords_cache`). | None. |
|
||||
| `ttf_words.c` font and raster cache | Hestia's framebuffer. | None; core 0 only, per plan. |
|
||||
|
||||
**Found by the sweep:**
|
||||
|
||||
| Item | Finding | Change |
|
||||
|---|---|---|
|
||||
| `heartbeat_export.c`, three function-local statics | `heartbeat_capture_tick_snapshot()` (every beat, `vm_runtime.c`) took each VM's deltas (beats, words executed, ANOVA early exits) against whichever VM was sampled last. Wrong across VMs on one core today. The snapshot feeds `doe_log_tick_row()` only, not any decision. | The previous values move into the VM's `HeartbeatState` (`export_last_*`). |
|
||||
| TOP-WORDS (`starforth_words.c`) | A static `DictEntry *[1000]`: shared by every VM, and silently cut at 1000 words. | Sized to the heated words and allocated per call. A refusal sets `vm->error` and is logged. |
|
||||
| `inference_outputs_to_string()` | Returned a static 256-byte buffer. No callers. | Takes a caller's buffer. |
|
||||
| Failure counters: word-id refusals, transition-grow failures, WORD too long | Plain `++`. | Atomic increments and loads. |
|
||||
| `block_subsystem.c` crc64 table | Lazy init with no publish ordering; `compute_crc64()` also runs outside the block lock. | Published with release/acquire. |
|
||||
|
||||
Left as system-wide by design: `system_running` and `forth_79_standard` (`system_words.c`, system flags). `profiler_state` and `vm_debug`'s `g_vm` are hosted/diagnostic; `vm_debug` is stubbed in the kernel.
|
||||
|
||||
The vendored hosted `make` builds clean.
|
||||
|
||||
**Probe** (temporary, removed before commit). An `MC0C-PROBE` word in Hera's and every child's vocabulary:
|
||||
- listed every VM's index, and how many times it has ever been rebuilt;
|
||||
- then looked up DUP, SWAP, EMIT, BLOCK and USE in two VMs alternately, 1,000 rounds each (5,000 lookups per VM);
|
||||
- checked that every result belongs to the dictionary of the VM that was searched.
|
||||
|
||||
It ran at Hera's prompt after O-A, and again on rajames after the standard sequence. The results are identical on all three ISAs:
|
||||
|
||||
| ISA | log | every VM has its own index | Hera rebuilds (her own defines/forgets) | every other VM's rebuilds | alternating 2×5000: rebuilds / found / wrong-VM | alloc failures |
|
||||
|---|---|---|---|---|---|---|
|
||||
| amd64 | `logs/20260925-194705` | yes (7 VMs) | 85 | 2 | 0+0 / 10000 / 0 | 0 |
|
||||
| aarch64 | `logs/20260925-200019` | yes (7 VMs) | 85 | 2 | 0+0 / 10000 / 0 | 0 |
|
||||
| riscv64 | `logs/20260925-201350` | yes (7 VMs) | 85 | 2 | 0+0 / 10000 / 0 | 0 |
|
||||
|
||||
These runs' `dict_hash` values differ from the table below only because the probe adds a word.
|
||||
|
||||
**Standard sequence** (clean build, with the probe reverted; the tree is byte-identical to the saved stage diff).
|
||||
|
||||
| ISA | log | O-A (G, PREC, LOAD) | M7.1a hash | MINT / WIREBIND / USE / `1 2 + .` | idle growth | UNKNOWN / faults |
|
||||
|---|---|---|---|---|---|---|
|
||||
| amd64 | `logs/20260925-202657` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
|
||||
| aarch64 | `logs/20260925-203935` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
|
||||
| riscv64 | `logs/20260925-205238` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
|
||||
|
||||
- **The `dict_hash` set is unchanged**, as the plan's evidence requires (the index is not hashed): Hera `0x00340a8b2c842eb7`, Hestia `0x78bf28439e4a643f`, Artemis `0x66886d174d5f3b5a`.
|
||||
- The G string is byte-identical to MC-0b2, MC-0b and MC-0a on all three ISAs.
|
||||
- The ledger line is unchanged.
|
||||
- No stop condition was hit. **MC-0c done.**
|
||||
|
||||
@@ -92,15 +92,35 @@ DictEntry *dict_find_word_heat_aware(VM *vm, const char *name, size_t len);
|
||||
*/
|
||||
void dict_adaptive_optimization_pass(VM *vm);
|
||||
|
||||
/* Frequency-class (fc) buckets — shared between dictionary_management.c and
|
||||
dictionary_heat_optimization.c. Declared here once to give LTO a single
|
||||
canonical type definition and avoid -Werror=lto-type-mismatch. */
|
||||
/* First-character (fc) lookup index — shared between dictionary_management.c
|
||||
and dictionary_heat_optimization.c. Declared here once to give LTO a single
|
||||
canonical type definition and avoid -Werror=lto-type-mismatch.
|
||||
|
||||
FABRIC-3.10.8 Stage MC-0c: one index per VM (vm->fc_index), not one for
|
||||
the whole system. The old global index held whichever VM's dictionary was
|
||||
searched last, so alternating between two VMs rebuilt it on every switch,
|
||||
and a heat reorganization could reorder another VM's buckets. SF_FC_BUCKETS
|
||||
is one bucket per possible first byte, the whole domain, not a capacity. */
|
||||
#ifndef SF_FC_BUCKETS
|
||||
#define SF_FC_BUCKETS 256
|
||||
#endif
|
||||
extern DictEntry **sf_fc_list[SF_FC_BUCKETS];
|
||||
extern size_t sf_fc_count[SF_FC_BUCKETS];
|
||||
extern size_t sf_fc_cap[SF_FC_BUCKETS];
|
||||
struct SfFcIndex {
|
||||
DictEntry **list[SF_FC_BUCKETS]; /* per bucket: entries, [0]=oldest */
|
||||
size_t count[SF_FC_BUCKETS];
|
||||
size_t cap[SF_FC_BUCKETS];
|
||||
DictEntry *cached_latest; /* the vm->latest this index reflects */
|
||||
uint64_t rebuilds; /* full rebuilds of this index, ever */
|
||||
};
|
||||
|
||||
/* This VM's index, allocated on first use; NULL only if that allocation
|
||||
failed (counted and logged -- lookups then walk the dictionary). */
|
||||
struct SfFcIndex *vm_dictionary_index(VM *vm);
|
||||
|
||||
/* Frees vm's index (vm_cleanup()). */
|
||||
void vm_dictionary_index_free(VM *vm);
|
||||
|
||||
/* Index allocations refused for lack of memory, system-wide. */
|
||||
uint32_t vm_dictionary_index_alloc_failures(void);
|
||||
|
||||
#endif /* DICTIONARY_HEAT_OPTIMIZATION_H */
|
||||
|
||||
|
||||
@@ -210,9 +210,12 @@ void inference_engine_run(InferenceInputs *inputs, InferenceOutputs *outputs);
|
||||
* Format: "window_width=XXX variance_q48=YYYY slope_q48=ZZZZ fit_quality=WWWW"
|
||||
*
|
||||
* @param outputs Inference outputs struct
|
||||
* @return Pointer to static string (valid until next call)
|
||||
* @param buf Caller's buffer (256 bytes holds any output) -- FABRIC-3.10.8
|
||||
* MC-0c: no longer a static buffer shared by every VM
|
||||
* @param cap Size of @p buf
|
||||
* @return @p buf
|
||||
*/
|
||||
const char* inference_outputs_to_string(const InferenceOutputs *outputs);
|
||||
const char* inference_outputs_to_string(const InferenceOutputs *outputs, char *buf, size_t cap);
|
||||
|
||||
/*
|
||||
* @brief Validate inference outputs for sanity
|
||||
|
||||
@@ -312,6 +312,13 @@ typedef struct
|
||||
uint64_t run_start_ns; /* Monotonic time at run start */
|
||||
uint32_t tick_number_offset; /* Tick counter for this run */
|
||||
|
||||
/* heartbeat_capture_tick_snapshot()'s previous values, for its deltas
|
||||
* (FABRIC-3.10.8 MC-0c: were function-local statics shared by every VM,
|
||||
* so each VM's delta was taken against whichever VM was sampled last). */
|
||||
uint64_t export_last_tick_count;
|
||||
uint64_t export_last_words_executed;
|
||||
uint64_t export_last_early_exit_count;
|
||||
|
||||
/* === L8 Attractor Bucket Statistics === */
|
||||
double bucket_sum_K; /* Sum of K values across bucket */
|
||||
double bucket_sum_K_squared; /* Sum of K^2 for variance calculation */
|
||||
@@ -594,6 +601,9 @@ typedef struct VM
|
||||
* @{
|
||||
*/
|
||||
HotwordsCache* hotwords_cache; /**< Frequency-driven dictionary acceleration cache */
|
||||
struct SfFcIndex *fc_index; /**< This VM's first-character lookup index
|
||||
* (FABRIC-3.10.8 MC-0c; dictionary_management.c,
|
||||
* allocated on first lookup, freed by vm_cleanup()) */
|
||||
/** @} */
|
||||
|
||||
/** @name Rolling Window of Truth (Deterministic Metrics)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -140,8 +140,12 @@ static inline uint32_t udiv_floor(uint32_t a, uint32_t b) { return a / b; }
|
||||
static uint64_t crc64_table[256];
|
||||
static int crc64_inited = 0;
|
||||
|
||||
/* FABRIC-3.10.8 MC-0c: compute_crc64() runs outside the block lock too (the
|
||||
* log and DoE regions, capsules), so the table is published with release/
|
||||
* acquire ordering -- a core that sees crc64_inited set also sees the whole
|
||||
* table. Two cores filling it at once write identical values. */
|
||||
static void crc64_init(void) {
|
||||
if (crc64_inited) return;
|
||||
if (__atomic_load_n(&crc64_inited, __ATOMIC_ACQUIRE)) return;
|
||||
const uint64_t poly = 0x42F0E1EBA9EA3693ULL;
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
uint64_t crc = (uint64_t) i;
|
||||
@@ -149,11 +153,11 @@ static void crc64_init(void) {
|
||||
crc = (crc & 1) ? ((crc >> 1) ^ poly) : (crc >> 1);
|
||||
crc64_table[i] = crc;
|
||||
}
|
||||
crc64_inited = 1;
|
||||
__atomic_store_n(&crc64_inited, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
uint64_t compute_crc64(const uint8_t *data, size_t len) {
|
||||
if (!crc64_inited) crc64_init();
|
||||
if (!__atomic_load_n(&crc64_inited, __ATOMIC_ACQUIRE)) crc64_init();
|
||||
uint64_t crc = 0xFFFFFFFFFFFFFFFFULL;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint8_t idx = (uint8_t)(crc ^ data[i]);
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
#include "log.h"
|
||||
#include "platform_time.h"
|
||||
|
||||
/* sf_fc_list/sf_fc_count/sf_fc_cap are declared in dictionary_heat_optimization.h */
|
||||
/* struct SfFcIndex (vm->fc_index, per VM since MC-0c) is declared in dictionary_heat_optimization.h */
|
||||
|
||||
/* ============================================================================
|
||||
* Heat Percentile Calculation
|
||||
@@ -161,7 +161,7 @@ void dict_update_heat_percentiles(VM *vm) {
|
||||
/**
|
||||
* @brief Three-pass heat-stratified dictionary lookup, hottest tier first.
|
||||
*
|
||||
* Searches the first-character bucket @c sf_fc_list[(unsigned char)name[0]]
|
||||
* Searches the first-character bucket @c vm->fc_index->list[(unsigned char)name[0]]
|
||||
* for a word whose @c name and @c name_len match @p name / @p len, scanning
|
||||
* in three priority tiers ordered by execution heat:
|
||||
*
|
||||
@@ -196,9 +196,15 @@ void dict_update_heat_percentiles(VM *vm) {
|
||||
DictEntry *dict_find_word_heat_aware(VM *vm, const char *name, size_t len) {
|
||||
if (!vm || !name || len == 0) return NULL;
|
||||
|
||||
/* MC-0c: vm's own index. vm_find_word(), the only caller, has already
|
||||
* brought it up to date (and walks the dictionary itself if there is
|
||||
* none). */
|
||||
struct SfFcIndex *ix = vm->fc_index;
|
||||
if (!ix) return NULL;
|
||||
|
||||
const unsigned char first = (unsigned char)name[0];
|
||||
DictEntry **bucket = sf_fc_list[first];
|
||||
size_t n = sf_fc_count[first];
|
||||
DictEntry **bucket = ix->list[first];
|
||||
size_t n = ix->count[first];
|
||||
|
||||
if (!bucket || n == 0) return NULL;
|
||||
|
||||
@@ -249,9 +255,9 @@ static int compare_dict_entry_heat_desc(const void *a, const void *b) {
|
||||
* Holds @c vm->dict_lock for the entire operation to prevent concurrent
|
||||
* @c DictEntry frees (from word redefinition or GC) from invalidating the
|
||||
* pointer array while it is being sorted. For each non-empty bucket in
|
||||
* @c sf_fc_list[0..255]:
|
||||
* @c vm->fc_index->list[0..255]:
|
||||
*
|
||||
* 1. Reads @c sf_fc_count[bucket_id] and @c sf_fc_cap[bucket_id]; skips the
|
||||
* 1. Reads the bucket's @c count and @c cap; skips the
|
||||
* bucket if @c count > @c cap (indicates a TOCTOU race — logs a warning).
|
||||
* 2. Allocates a temporary copy of the bucket pointer array.
|
||||
* 3. Re-verifies @c count has not changed (TOCTOU double-check); if it has,
|
||||
@@ -277,14 +283,18 @@ void dict_reorganize_buckets_by_heat(VM *vm) {
|
||||
* Without this, main thread can free DictEntry objects while we're sorting pointers */
|
||||
sf_mutex_lock(&vm->dict_lock);
|
||||
|
||||
/* MC-0c: this VM's own buckets -- before, whichever VM's dictionary had
|
||||
* been searched last. */
|
||||
struct SfFcIndex *ix = vm->fc_index;
|
||||
|
||||
/* Reorganize each bucket by heat (hottest words first) */
|
||||
for (int bucket_id = 0; bucket_id < 256; bucket_id++) {
|
||||
if (!sf_fc_list[bucket_id] || sf_fc_count[bucket_id] == 0) continue;
|
||||
for (int bucket_id = 0; ix && bucket_id < SF_FC_BUCKETS; bucket_id++) {
|
||||
if (!ix->list[bucket_id] || ix->count[bucket_id] == 0) continue;
|
||||
|
||||
/* ASan fix: Verify count doesn't exceed capacity before accessing (2025-12-09)
|
||||
* Race condition: Main thread may have changed count/capacity while we read it */
|
||||
size_t count = sf_fc_count[bucket_id];
|
||||
size_t cap = sf_fc_cap[bucket_id];
|
||||
size_t count = ix->count[bucket_id];
|
||||
size_t cap = ix->cap[bucket_id];
|
||||
if (count > cap) {
|
||||
log_message(LOG_WARN,
|
||||
"dict_reorganize_buckets_by_heat: skipping bucket %d - count=%zu exceeds capacity=%zu (race condition)",
|
||||
@@ -297,7 +307,7 @@ void dict_reorganize_buckets_by_heat(VM *vm) {
|
||||
if (!bucket_copy) continue;
|
||||
|
||||
/* Re-verify count hasn't changed (TOCTOU protection) */
|
||||
size_t count_recheck = sf_fc_count[bucket_id];
|
||||
size_t count_recheck = ix->count[bucket_id];
|
||||
if (count_recheck != count || count_recheck > cap) {
|
||||
log_message(LOG_WARN,
|
||||
"dict_reorganize_buckets_by_heat: bucket %d count changed (%zu -> %zu), aborting",
|
||||
@@ -306,14 +316,14 @@ void dict_reorganize_buckets_by_heat(VM *vm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
memcpy(bucket_copy, sf_fc_list[bucket_id], count * sizeof(DictEntry *));
|
||||
memcpy(bucket_copy, ix->list[bucket_id], count * sizeof(DictEntry *));
|
||||
|
||||
/* Sort by descending heat (use cached count to avoid TOCTOU race) */
|
||||
qsort(bucket_copy, count, sizeof(DictEntry *),
|
||||
compare_dict_entry_heat_desc);
|
||||
|
||||
/* Copy back to original bucket (still use cached count) */
|
||||
memcpy(sf_fc_list[bucket_id], bucket_copy, count * sizeof(DictEntry *));
|
||||
memcpy(ix->list[bucket_id], bucket_copy, count * sizeof(DictEntry *));
|
||||
|
||||
free(bucket_copy);
|
||||
}
|
||||
|
||||
+81
-59
@@ -61,19 +61,10 @@
|
||||
#endif
|
||||
/* LIKELY/UNLIKELY macros are defined in vm.h */
|
||||
|
||||
#ifndef SF_FC_BUCKETS
|
||||
#define SF_FC_BUCKETS 256
|
||||
#endif
|
||||
|
||||
/* Buckets of entry pointers, sizes, and capacities (we reuse memory). */
|
||||
/* Note: sf_fc_list, sf_fc_count, and sf_fc_cap are not static to allow access from
|
||||
* dictionary_heat_optimization.c for heat-aware bucket reorganization with proper
|
||||
* capacity checks (ASan fix: 2025-12-09). */
|
||||
DictEntry **sf_fc_list[SF_FC_BUCKETS];
|
||||
size_t sf_fc_count[SF_FC_BUCKETS];
|
||||
size_t sf_fc_cap[SF_FC_BUCKETS];
|
||||
|
||||
static DictEntry *sf_cached_latest = NULL; /* last head we indexed */
|
||||
/* The first-character lookup index is per VM (vm->fc_index, struct SfFcIndex
|
||||
* in dictionary_heat_optimization.h; FABRIC-3.10.8 Stage MC-0c). Buckets
|
||||
* reuse their memory across rebuilds. */
|
||||
static uint32_t g_fc_index_alloc_failures; /* atomic: any core can count one */
|
||||
|
||||
/* FABRIC-3.9.0.md §8.1 (Stage S-1b): stable word ids are no longer capped at
|
||||
* DICTIONARY_SIZE. The id tables grow with the dictionary; a word that can't
|
||||
@@ -84,7 +75,7 @@ static DictEntry *sf_cached_latest = NULL; /* last head we indexed */
|
||||
|
||||
static uint32_t g_word_id_refusals;
|
||||
|
||||
uint32_t vm_dictionary_word_id_refusals(void) { return g_word_id_refusals; }
|
||||
uint32_t vm_dictionary_word_id_refusals(void) { return __atomic_load_n(&g_word_id_refusals, __ATOMIC_RELAXED); }
|
||||
|
||||
/* Grow both id tables to at least `need` entries, all-or-nothing: both new
|
||||
* tables are allocated before either old one is touched, so a failure leaves
|
||||
@@ -160,7 +151,7 @@ int vm_dictionary_track_entry(VM *vm, DictEntry *entry) {
|
||||
entry->word_id = word_id;
|
||||
|
||||
if (word_id == WORD_ID_INVALID) {
|
||||
g_word_id_refusals++;
|
||||
__atomic_add_fetch(&g_word_id_refusals, 1, __ATOMIC_RELAXED); /* MC-0c: any core */
|
||||
log_message(LOG_ERROR, "word id table can't grow: '%.*s' refused",
|
||||
(int)entry->name_len, entry->name);
|
||||
return -1;
|
||||
@@ -248,21 +239,39 @@ static inline void *sf_xrealloc(void *p, size_t nbytes) {
|
||||
return r;
|
||||
}
|
||||
|
||||
static void sf_fc_clear_all(void) {
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
|
||||
sf_free(sf_fc_list[i]);
|
||||
sf_fc_list[i] = NULL;
|
||||
sf_fc_count[i] = 0;
|
||||
sf_fc_cap[i] = 0;
|
||||
struct SfFcIndex *vm_dictionary_index(VM *vm) {
|
||||
if (!vm) return NULL;
|
||||
if (!vm->fc_index) {
|
||||
vm->fc_index = (struct SfFcIndex *) sf_calloc(1, sizeof(struct SfFcIndex));
|
||||
if (!vm->fc_index) {
|
||||
/* Lookups stay correct without it (vm_find_word() walks the
|
||||
* dictionary); counted and logged, retried on the next lookup. */
|
||||
uint32_t n = __atomic_add_fetch(&g_fc_index_alloc_failures, 1, __ATOMIC_RELAXED);
|
||||
if (n == 1 || (n & (n - 1)) == 0)
|
||||
log_message(LOG_ERROR, "dictionary index: can't allocate (x%u) -- "
|
||||
"lookups walk the dictionary", (unsigned) n);
|
||||
}
|
||||
}
|
||||
return vm->fc_index;
|
||||
}
|
||||
|
||||
uint32_t vm_dictionary_index_alloc_failures(void) {
|
||||
return __atomic_load_n(&g_fc_index_alloc_failures, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
void vm_dictionary_index_free(VM *vm) {
|
||||
if (!vm || !vm->fc_index) return;
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) sf_free(vm->fc_index->list[i]);
|
||||
sf_free(vm->fc_index);
|
||||
vm->fc_index = NULL;
|
||||
}
|
||||
|
||||
/* Ensure bucket i has at least `need` capacity; grow geometrically. */
|
||||
static int sf_fc_reserve(size_t i, size_t need) {
|
||||
if (sf_fc_cap[i] >= need) return 1;
|
||||
size_t newcap = sf_fc_cap[i] ? sf_fc_cap[i] : 8;
|
||||
static int sf_fc_reserve(struct SfFcIndex *ix, size_t i, size_t need) {
|
||||
if (ix->cap[i] >= need) return 1;
|
||||
size_t newcap = ix->cap[i] ? ix->cap[i] : 8;
|
||||
while (newcap < need) newcap <<= 1;
|
||||
DictEntry **old = sf_fc_list[i];
|
||||
DictEntry **old = ix->list[i];
|
||||
DictEntry **newv = (DictEntry **) sf_xrealloc(old, newcap * sizeof(*newv));
|
||||
if (!newv) return 0;
|
||||
/* Kernel sf_realloc is a bump allocator that does not copy; patch that up.
|
||||
@@ -270,66 +279,67 @@ static int sf_fc_reserve(size_t i, size_t need) {
|
||||
* from freed memory (UB; glibc overwrites freed block with heap metadata). */
|
||||
#ifdef __STARKERNEL__
|
||||
{
|
||||
size_t old_count = sf_fc_count[i];
|
||||
size_t old_count = ix->count[i];
|
||||
if (newv != old && old && old_count > 0)
|
||||
memcpy(newv, old, old_count * sizeof(*newv));
|
||||
}
|
||||
#endif
|
||||
sf_fc_list[i] = newv;
|
||||
sf_fc_cap[i] = newcap;
|
||||
ix->list[i] = newv;
|
||||
ix->cap[i] = newcap;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Full rebuild: count → reserve → fill oldest→newest (so backward scan = newest-first). */
|
||||
static void sf_rebuild_fc_index(VM *vm) {
|
||||
static void sf_rebuild_fc_index(VM *vm, struct SfFcIndex *ix) {
|
||||
/* Reset counts, keep capacity to avoid churn. */
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) sf_fc_count[i] = 0;
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) ix->count[i] = 0;
|
||||
|
||||
/* Count first chars. */
|
||||
for (DictEntry *e = vm->latest; e; e = e->link) {
|
||||
unsigned fc = (unsigned char) e->name[0];
|
||||
sf_fc_count[fc]++;
|
||||
ix->count[fc]++;
|
||||
}
|
||||
|
||||
/* Reserve memory once per bucket. */
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
|
||||
if (sf_fc_count[i] && !sf_fc_reserve(i, sf_fc_count[i])) {
|
||||
if (ix->count[i] && !sf_fc_reserve(ix, i, ix->count[i])) {
|
||||
/* OOM for this bucket → treat as empty; we’ll just miss the fast path. */
|
||||
sf_fc_count[i] = 0;
|
||||
ix->count[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Back-fill so [0]=oldest, [count-1]=newest. */
|
||||
size_t fill_at[SF_FC_BUCKETS];
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) fill_at[i] = sf_fc_count[i];
|
||||
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) fill_at[i] = ix->count[i];
|
||||
|
||||
for (DictEntry *e = vm->latest; e; e = e->link) {
|
||||
unsigned fc = (unsigned char) e->name[0];
|
||||
if (sf_fc_count[fc] == 0 || !sf_fc_list[fc]) continue;
|
||||
sf_fc_list[fc][--fill_at[fc]] = e;
|
||||
if (ix->count[fc] == 0 || !ix->list[fc]) continue;
|
||||
ix->list[fc][--fill_at[fc]] = e;
|
||||
}
|
||||
|
||||
sf_cached_latest = vm->latest;
|
||||
ix->cached_latest = vm->latest;
|
||||
ix->rebuilds++;
|
||||
}
|
||||
|
||||
/* Fast path: if exactly one new word was pushed, append it without rebuilding. */
|
||||
static void sf_try_fast_append(VM *vm) {
|
||||
if (!vm || vm->latest == sf_cached_latest) return;
|
||||
static void sf_try_fast_append(VM *vm, struct SfFcIndex *ix) {
|
||||
if (!vm || vm->latest == ix->cached_latest) return;
|
||||
DictEntry *newest = vm->latest;
|
||||
/* single push if the new head links to the previously cached head */
|
||||
if (newest && newest->link == sf_cached_latest) {
|
||||
if (newest && newest->link == ix->cached_latest) {
|
||||
unsigned fc = (unsigned char) newest->name[0];
|
||||
size_t n = sf_fc_count[fc];
|
||||
if (sf_fc_reserve(fc, n + 1)) {
|
||||
size_t n = ix->count[fc];
|
||||
if (sf_fc_reserve(ix, fc, n + 1)) {
|
||||
/* Keep oldest→…→newest ordering: append at the end. */
|
||||
sf_fc_list[fc][n] = newest;
|
||||
sf_fc_count[fc] = n + 1;
|
||||
sf_cached_latest = newest;
|
||||
ix->list[fc][n] = newest;
|
||||
ix->count[fc] = n + 1;
|
||||
ix->cached_latest = newest;
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Otherwise fall back to a full rebuild (multiple changes, deletes, etc.). */
|
||||
sf_rebuild_fc_index(vm);
|
||||
sf_rebuild_fc_index(vm, ix);
|
||||
}
|
||||
|
||||
/* --- API ----------------------------------------------------------------- */
|
||||
@@ -401,12 +411,32 @@ DictEntry *vm_find_word(VM *vm, const char *name, size_t len) {
|
||||
/* Thread safety: Acquire dict_lock for read access to dictionary */
|
||||
sf_mutex_lock(&vm->dict_lock);
|
||||
|
||||
/* This VM's own index (MC-0c). Without one (allocation refused, counted
|
||||
* in vm_dictionary_index()), walk the dictionary newest-first: the
|
||||
* first visible match is the newest definition. */
|
||||
struct SfFcIndex *ix = vm_dictionary_index(vm);
|
||||
if (UNLIKELY(!ix)) {
|
||||
DictEntry *w;
|
||||
for (w = vm->latest; w; w = w->link) {
|
||||
if ((size_t) w->name_len != len || memcmp(w->name, name, len) != 0) continue;
|
||||
#ifdef WORD_HIDDEN
|
||||
if (w->flags & WORD_HIDDEN) continue;
|
||||
#endif
|
||||
#ifdef WORD_SMUDGED
|
||||
if (w->flags & WORD_SMUDGED) continue;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
sf_mutex_unlock(&vm->dict_lock);
|
||||
return w;
|
||||
}
|
||||
|
||||
/* Keep the index in sync with dictionary head, cheaply if possible. */
|
||||
if (UNLIKELY(sf_cached_latest != vm->latest)) sf_try_fast_append(vm);
|
||||
if (UNLIKELY(ix->cached_latest != vm->latest)) sf_try_fast_append(vm, ix);
|
||||
|
||||
const unsigned char first = (unsigned char) name[0];
|
||||
DictEntry **bucket = sf_fc_list[first];
|
||||
size_t n = sf_fc_count[first];
|
||||
DictEntry **bucket = ix->list[first];
|
||||
size_t n = ix->count[first];
|
||||
if (UNLIKELY(!bucket || n == 0)) {
|
||||
sf_mutex_unlock(&vm->dict_lock);
|
||||
return NULL;
|
||||
@@ -662,13 +692,5 @@ void vm_unpin_execution_heat(VM *vm) {
|
||||
physics_metadata_refresh_state(vm->latest);
|
||||
}
|
||||
|
||||
/* If you ever need to hard-reset (e.g., switching vocabularies wholesale) */
|
||||
/**
|
||||
* @brief Resets the dictionary index
|
||||
*
|
||||
* Clears all index data and cached state. Used when switching vocabularies.
|
||||
*/
|
||||
void vm_dictionary_index_reset(void) {
|
||||
sf_cached_latest = NULL;
|
||||
sf_fc_clear_all();
|
||||
}
|
||||
/* MC-0c: vm_dictionary_index_reset() (the global index's hard reset, no
|
||||
* callers) is gone; a VM's index is freed with the VM, vm_dictionary_index_free(). */
|
||||
|
||||
+12
-13
@@ -70,13 +70,14 @@ void heartbeat_capture_tick_snapshot(VM* vm, HeartbeatTickSnapshot* snapshot)
|
||||
* raw counter on riscv64). Use nominal tick period instead. */
|
||||
snapshot->elapsed_ns = hb->tick_count * (uint64_t)HEARTBEAT_TICK_NS;
|
||||
|
||||
static uint64_t last_tick_count = 0;
|
||||
uint64_t delta_ticks = hb->tick_count - last_tick_count;
|
||||
uint64_t delta_ticks = hb->tick_count - hb->export_last_tick_count;
|
||||
snapshot->tick_interval_ns = delta_ticks * (uint64_t)HEARTBEAT_TICK_NS;
|
||||
last_tick_count = hb->tick_count;
|
||||
hb->export_last_tick_count = hb->tick_count;
|
||||
|
||||
/* Delta metrics — computed from per-VM counters using static last-value tracking.
|
||||
* On VM reset (current < last), treat current value as the full delta. */
|
||||
/* Delta metrics — computed from per-VM counters against this VM's own
|
||||
* previous values (hb->export_last_*; FABRIC-3.10.8 MC-0c -- were statics
|
||||
* shared by every VM). On VM reset (current < last), treat current value
|
||||
* as the full delta. */
|
||||
|
||||
/* Cache metrics from hotwords cache */
|
||||
snapshot->cache_hits_delta = 0; /* TODO: wire hotwords cache hit counter */
|
||||
@@ -85,12 +86,11 @@ void heartbeat_capture_tick_snapshot(VM* vm, HeartbeatTickSnapshot* snapshot)
|
||||
/* Word execution delta — words executed since the previous heartbeat tick.
|
||||
* vm->heartbeat.words_executed is incremented by vm_core.c on every dispatch. */
|
||||
{
|
||||
static uint64_t last_words_executed = 0;
|
||||
uint64_t cur = hb->words_executed;
|
||||
snapshot->word_executions_delta = (uint32_t)(cur >= last_words_executed
|
||||
? cur - last_words_executed
|
||||
snapshot->word_executions_delta = (uint32_t)(cur >= hb->export_last_words_executed
|
||||
? cur - hb->export_last_words_executed
|
||||
: cur);
|
||||
last_words_executed = cur;
|
||||
hb->export_last_words_executed = cur;
|
||||
}
|
||||
|
||||
/* Hot word count - walk dictionary and count words above threshold */
|
||||
@@ -141,12 +141,11 @@ void heartbeat_capture_tick_snapshot(VM* vm, HeartbeatTickSnapshot* snapshot)
|
||||
* High rate = selector chose well and the system settled.
|
||||
* Low rate = selector is still searching. */
|
||||
{
|
||||
static uint64_t last_early_exit_count = 0;
|
||||
uint64_t cur = hb->early_exit_count;
|
||||
snapshot->predicted_label_hits = (uint32_t)(cur >= last_early_exit_count
|
||||
? cur - last_early_exit_count
|
||||
snapshot->predicted_label_hits = (uint32_t)(cur >= hb->export_last_early_exit_count
|
||||
? cur - hb->export_last_early_exit_count
|
||||
: cur);
|
||||
last_early_exit_count = cur;
|
||||
hb->export_last_early_exit_count = cur;
|
||||
}
|
||||
|
||||
/* Jitter: tick-based timing has no wall-clock deviation to measure.
|
||||
|
||||
@@ -756,18 +756,21 @@ void inference_engine_run(InferenceInputs *inputs, InferenceOutputs *outputs)
|
||||
/**
|
||||
* @brief Format inference outputs as a human-readable string.
|
||||
*
|
||||
* Writes into a static 256-byte buffer — not thread-safe, for diagnostics only.
|
||||
* Writes into the caller's buffer (FABRIC-3.10.8 MC-0c: was a static
|
||||
* 256-byte buffer shared by every VM and core).
|
||||
* Format: @c "window=N var=F slope=F quality=F (cached|full)".
|
||||
*
|
||||
* @param outputs Inference outputs to format (NULL produces @c "(null)")
|
||||
* @return Pointer to a static string buffer
|
||||
* @param buf Destination (256 bytes holds any output)
|
||||
* @param cap Size of @p buf
|
||||
* @return @p buf, or "" if @p buf is NULL or @p cap is 0
|
||||
*/
|
||||
const char* inference_outputs_to_string(const InferenceOutputs *outputs)
|
||||
const char* inference_outputs_to_string(const InferenceOutputs *outputs, char *buf, size_t cap)
|
||||
{
|
||||
static char buf[256];
|
||||
if (!buf || cap == 0) return "";
|
||||
|
||||
if (!outputs) {
|
||||
snprintf(buf, sizeof(buf), "(null)");
|
||||
snprintf(buf, cap, "(null)");
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -775,7 +778,7 @@ const char* inference_outputs_to_string(const InferenceOutputs *outputs)
|
||||
double slope_dbl = q48_to_double(outputs->adaptive_decay_slope);
|
||||
double quality_dbl = q48_to_double(outputs->slope_fit_quality_q48);
|
||||
|
||||
snprintf(buf, sizeof(buf),
|
||||
snprintf(buf, cap,
|
||||
"window=%u var=%.6f slope=%.6f quality=%.6f %s",
|
||||
outputs->adaptive_window_width,
|
||||
var_dbl,
|
||||
|
||||
@@ -212,7 +212,7 @@ int transition_metrics_init(WordTransitionMetrics *metrics) {
|
||||
*/
|
||||
static uint64_t g_transition_grow_failures;
|
||||
|
||||
uint64_t transition_metrics_grow_failures(void) { return g_transition_grow_failures; }
|
||||
uint64_t transition_metrics_grow_failures(void) { return __atomic_load_n(&g_transition_grow_failures, __ATOMIC_RELAXED); }
|
||||
|
||||
int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word_id, uint32_t dict_size) {
|
||||
if (!metrics || next_word_id >= dict_size) {
|
||||
@@ -234,7 +234,7 @@ int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word
|
||||
}
|
||||
grown = (uint64_t *)calloc(new_cap, sizeof(uint64_t));
|
||||
if (!grown) {
|
||||
g_transition_grow_failures++;
|
||||
__atomic_add_fetch(&g_transition_grow_failures, 1, __ATOMIC_RELAXED); /* MC-0c: any core */
|
||||
log_message(LOG_ERROR, "transition_heat can't grow: dropped");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -312,6 +312,9 @@ void vm_cleanup(VM* vm)
|
||||
vm->hotwords_cache = NULL;
|
||||
}
|
||||
|
||||
/* This VM's dictionary lookup index (FABRIC-3.10.8 MC-0c) */
|
||||
vm_dictionary_index_free(vm);
|
||||
|
||||
/* Clean up rolling window of truth */
|
||||
rolling_window_cleanup(&vm->rolling_window);
|
||||
|
||||
|
||||
@@ -394,6 +394,9 @@ void vm_cleanup(VM* vm)
|
||||
vm->hotwords_cache = NULL;
|
||||
}
|
||||
|
||||
/* This VM's dictionary lookup index (FABRIC-3.10.8 MC-0c) */
|
||||
vm_dictionary_index_free(vm);
|
||||
|
||||
/* Clean up rolling window of truth */
|
||||
rolling_window_cleanup(&vm->rolling_window);
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@
|
||||
#include "../../include/word_registry.h"
|
||||
#include "../../include/log.h"
|
||||
|
||||
/* Global state for dictionary manipulation */
|
||||
static cell_t state_variable = 0; /* STATE - compilation state */
|
||||
/* FABRIC-3.10.8 MC-0c: the file-global state_variable ("STATE") that lived
|
||||
* here is gone. It was written (INTERPRET) and never read -- STATE is
|
||||
* vm->mode, already per VM. */
|
||||
|
||||
/* Helper function to calculate aligned address */
|
||||
static uintptr_t align_address(uintptr_t addr) {
|
||||
@@ -360,7 +361,6 @@ void dictionary_m_word_interpret(VM *vm) {
|
||||
|
||||
/* Set interpretation mode */
|
||||
vm->mode = MODE_INTERPRET;
|
||||
state_variable = 0;
|
||||
|
||||
/* The actual interpretation happens through vm_interpret() calls */
|
||||
/* This word exists mainly for completeness and mode setting */
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
#include "platform_time.h"
|
||||
#include "word_registry.h"
|
||||
|
||||
/* Forward declarations */
|
||||
extern DictEntry **sf_fc_list[256];
|
||||
extern size_t sf_fc_count[256];
|
||||
|
||||
/* ============================================================================
|
||||
* Benchmark: Dictionary Lookup Performance
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
#include "../../include/platform_time.h"
|
||||
#include "../../include/profiler.h"
|
||||
#include "../../include/block_subsystem.h"
|
||||
#include "../../include/platform_alloc.h" /* sf_malloc()/sf_free() -- TOP-WORDS (MC-0c) */
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -228,17 +229,33 @@ void starforth_word_top_words(VM* vm)
|
||||
printf("Top %ld most frequently used words:\n", (long)n);
|
||||
printf("==================================\n");
|
||||
|
||||
/* Collect all words with execution_heat > 0 (static: avoids large kernel stack frame) */
|
||||
static DictEntry* words_with_heat[1000];
|
||||
/* Collect all words with execution_heat > 0. FABRIC-3.10.8 MC-0c: sized
|
||||
* to this dictionary and allocated per call -- was a static [1000]
|
||||
* (shared by every VM, and silently cut at 1000 words). */
|
||||
int word_count = 0;
|
||||
|
||||
for (DictEntry* entry = vm->latest; entry && word_count < 1000; entry = entry->link)
|
||||
for (DictEntry* entry = vm->latest; entry; entry = entry->link)
|
||||
{
|
||||
if (entry->execution_heat > 0)
|
||||
if (entry->execution_heat > 0) word_count++;
|
||||
}
|
||||
DictEntry** words_with_heat = NULL;
|
||||
if (word_count > 0)
|
||||
{
|
||||
words_with_heat = (DictEntry**)sf_malloc((size_t)word_count * sizeof(DictEntry*));
|
||||
if (!words_with_heat)
|
||||
{
|
||||
words_with_heat[word_count++] = entry;
|
||||
vm->error = 1;
|
||||
log_message(LOG_ERROR, "TOP-WORDS: can't allocate a list of %d words", word_count);
|
||||
return;
|
||||
}
|
||||
}
|
||||
{
|
||||
int k = 0;
|
||||
for (DictEntry* entry = vm->latest; entry && k < word_count; entry = entry->link)
|
||||
{
|
||||
if (entry->execution_heat > 0) words_with_heat[k++] = entry;
|
||||
}
|
||||
word_count = k;
|
||||
}
|
||||
|
||||
/* Simple bubble sort by execution_heat (descending) */
|
||||
for (int i = 0; i < word_count - 1; i++)
|
||||
@@ -261,6 +278,7 @@ void starforth_word_top_words(VM* vm)
|
||||
DictEntry* entry = words_with_heat[i];
|
||||
printf("%d. %.*s: %ld\n", i + 1, (int)entry->name_len, entry->name, (long)entry->execution_heat);
|
||||
}
|
||||
sf_free(words_with_heat);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -653,9 +671,18 @@ void starforth_word_init(VM* vm)
|
||||
*/
|
||||
static uint64_t prng_next(void)
|
||||
{
|
||||
/* LCG: state = (a * state + c) mod m, where m = 2^64 (implicit) */
|
||||
g_prng_state = g_prng_state * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
return g_prng_state;
|
||||
/* LCG: state = (a * state + c) mod m, where m = 2^64 (implicit).
|
||||
* FABRIC-3.10.8 MC-0c: one stream for every VM (as accepted at Stage H),
|
||||
* stepped with compare-and-swap so two cores never draw the same value
|
||||
* or lose a step. Which core draws which value is ordering, not
|
||||
* physics (MC-R4). */
|
||||
uint64_t cur = __atomic_load_n(&g_prng_state, __ATOMIC_RELAXED);
|
||||
uint64_t next;
|
||||
do {
|
||||
next = cur * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
} while (!__atomic_compare_exchange_n(&g_prng_state, &cur, next, 0,
|
||||
__ATOMIC_RELAXED, __ATOMIC_RELAXED));
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -675,15 +702,16 @@ void starforth_word_seed(VM* vm)
|
||||
}
|
||||
|
||||
cell_t seed = vm_pop(vm);
|
||||
g_prng_state = (uint64_t)seed;
|
||||
uint64_t s = (uint64_t)seed;
|
||||
|
||||
/* Ensure non-zero state (LCG weakness) */
|
||||
if (g_prng_state == 0)
|
||||
if (s == 0)
|
||||
{
|
||||
g_prng_state = 1;
|
||||
s = 1;
|
||||
}
|
||||
__atomic_store_n(&g_prng_state, s, __ATOMIC_RELAXED); /* MC-0c: one store, not two */
|
||||
|
||||
log_message(LOG_DEBUG, "SEED: PRNG seeded with %lu", (unsigned long)g_prng_state);
|
||||
log_message(LOG_DEBUG, "SEED: PRNG seeded with %lu", (unsigned long)s);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -80,7 +80,7 @@ static inline int ensure_input(VM *vm) {
|
||||
* counted here -- never silently truncated. */
|
||||
static uint32_t g_word_too_long;
|
||||
|
||||
uint32_t string_word_word_refusals(void) { return g_word_too_long; }
|
||||
uint32_t string_word_word_refusals(void) { return __atomic_load_n(&g_word_too_long, __ATOMIC_RELAXED); }
|
||||
|
||||
/* Helper function to convert string to number (base-10 only here) */
|
||||
static int convert_string_to_number(const char *str, size_t len, cell_t *result) {
|
||||
@@ -254,7 +254,7 @@ void string_word_word(VM *vm) {
|
||||
if (word_len > WORD_BUFFER_SIZE - 2) {
|
||||
/* Refused whole: the delimiter goes back on the stack and >IN is
|
||||
* left where it was, so nothing is consumed. */
|
||||
g_word_too_long++;
|
||||
__atomic_add_fetch(&g_word_too_long, 1, __ATOMIC_RELAXED); /* MC-0c: any core */
|
||||
log_message(LOG_ERROR, "WORD: token over 255 chars refused");
|
||||
vm_push(vm, c);
|
||||
vm->error = 1;
|
||||
|
||||
Reference in New Issue
Block a user