Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions dag/gunbc/char_at_scaling_probe_support.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module gunbc.char_at_scaling_probe_support

import std.types { NonEmptyStr }
import std.disposition { Disposition, Terminal }
import std.dissolution { DissolutionCondition, unbound_dissolution }

// Per DESIGN §4b: this probe IS the discriminating evidence for CHARAT-0's O(1)
// char_at claim, so it stays enrolled rather than dissolving — the disposition is
// Terminal, not Scaffold. §5's same-unit rule is why this carries its own
// independent trigger rather than sharing json_parse_scaling_probe.rs's: the two
// probes measure different primitives (char_at cost in isolation vs. end-to-end
// parse) and could have independently different lifetimes.
data char_at_scaling_probe_disposition: Disposition = Terminal {
reason: "the entry point `src/v1/stage0/src/bin/char_at_scaling_probe.rs` resolves against — a single `char_at` call, isolated from every other primitive, so per-access cost can be fit against position and against string length without the JSON parser's independent list-materialization cost. NOT floor-enrolled; the acceptance evidence for CHARAT-0's O(1) claim."
}

// §3: cite the symbol, not a position or another file's marker. The bin's own
// grep receipt is `CHAR_AT_SCALING_PROBE_SCAFFOLD_MARKER` in
// `char_at_scaling_probe.rs` — a real, distinct const from
// `json_parse_scaling_probe.rs`'s `JSON_PARSE_SCALING_PROBE_SCAFFOLD_MARKER`,
// which this probe's disposition no longer cites or depends on.
data char_at_scaling_probe_dissolution: DissolutionCondition = unbound_dissolution(
description: "delete `src/v1/stage0/src/bin/char_at_scaling_probe.rs` (grep receipt: `CHAR_AT_SCALING_PROBE_SCAFFOLD_MARKER`) when char_at's O(1) property is floor-enrolled with a modeled witness, or when a fresh run of this probe's own printed TSV (mean_call_us per string_len/position, per its CHECKABLE RECEIPT doc comment) shows mean_call_us growing with string_len across the CHAR_AT_PROBE_LENGTHS range instead of staying flat" as NonEmptyStr
)

fn char_at_probe(s: String, pos: Int) -> String {
char_at(s: s, pos: pos)
}
24 changes: 19 additions & 5 deletions src/v1/runtime_rust.dag
Original file line number Diff line number Diff line change
Expand Up @@ -95,23 +95,34 @@ fn rt_concat_trait() -> String {

fn rt_string_ops() -> String {
concat(
"pub fn char_at(s: &str, pos: i64) -> String {\n",
"/// Ascii-aware variant taking a precomputed `is_ascii` flag (the `RcStr` carrier fact)\n",
"/// instead of rescanning the whole string on every call -- the per-call `s.is_ascii()`\n",
"/// scan is what made repeated indexing over a large string O(n^2) (STRING-INDEX-0).\n",
"pub fn char_at_ascii_aware(s: &str, is_ascii: bool, pos: i64) -> String {\n",
" let pos = pos.max(0) as usize;\n",
" if s.is_ascii() {\n",
" if is_ascii {\n",
" let bytes = s.as_bytes();\n",
" if pos >= bytes.len() { return String::new(); }\n",
" return String::from(bytes[pos] as char);\n",
" }\n",
" s.chars().nth(pos).map(|ch| ch.to_string()).unwrap_or_default()\n",
"}\n\n",
"pub fn char_at(s: &str, pos: i64) -> String {\n",
" char_at_ascii_aware(s, s.is_ascii(), pos)\n",
"}\n\n",
"/// Ascii-aware variant taking a precomputed `is_ascii` flag; see `char_at_ascii_aware`.\n",
"pub fn string_length_ascii_aware(s: &str, is_ascii: bool) -> i64 {\n",
" if is_ascii { s.len() as i64 } else { s.chars().count() as i64 }\n",
"}\n\n",
"pub fn string_length(s: &str) -> i64 {\n",
" if s.is_ascii() { s.len() as i64 } else { s.chars().count() as i64 }\n",
" string_length_ascii_aware(s, s.is_ascii())\n",
"}\n\n",
"pub fn substring(s: &str, start: i64, end: i64) -> String {\n",
"/// Ascii-aware variant taking a precomputed `is_ascii` flag; see `char_at_ascii_aware`.\n",
"pub fn substring_ascii_aware(s: &str, is_ascii: bool, start: i64, end: i64) -> String {\n",
" let start = start.max(0) as usize;\n",
" let end = end.max(0) as usize;\n",
" if end <= start { return String::new(); }\n",
" if s.is_ascii() {\n",
" if is_ascii {\n",
" let len = s.len();\n",
" if start >= len { return String::new(); }\n",
" let out_end = end.min(len);\n",
Expand All @@ -123,6 +134,9 @@ fn rt_string_ops() -> String {
" record_substring_chars_walked(s, start, take_len);\n",
" s.chars().skip(start).take(take_len).collect()\n",
"}\n\n",
"pub fn substring(s: &str, start: i64, end: i64) -> String {\n",
" substring_ascii_aware(s, s.is_ascii(), start, end)\n",
"}\n\n",
"pub fn string_contains(s: &str, sub: String) -> bool {\n",
" s.contains(&*sub)\n",
"}\n\n",
Expand Down
7 changes: 7 additions & 0 deletions src/v1/stage0/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ path = "src/bin/claim_executor.rs"
name = "json_parse_scaling_probe"
path = "src/bin/json_parse_scaling_probe.rs"

# Isolated char_at scaling receipt: position x string-length, uncontaminated by the
# JSON parser's independent list-accumulation cost (eager-koi-458 scope correction,
# 2026-08-17). DISSOLUTION: delete alongside json_parse_scaling_probe.rs.
[[bin]]
name = "char_at_scaling_probe"
path = "src/bin/char_at_scaling_probe.rs"

# Resolved-type owned-data discovery for Consolidation #4553 glob discovery.
# Hand-written CI tool (like claim_batch); exposes neutral decl facts only.
[[bin]]
Expand Down
142 changes: 142 additions & 0 deletions src/v1/stage0/src/bin/char_at_scaling_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#![allow(clippy::disallowed_macros)]

//! SCAFFOLD (DESIGN §7 seed-retained HAND-RUST / CHARAT-0) — isolated `char_at` scaling
//! receipt: many repeated calls at varying position and varying string length, uncontaminated
//! by the JSON parser's independent `value_to_list_carrier` / `free_monoid_to_vec`
//! materialization cost (a different, separately scoped defect — eager-koi-458's 2026-08-17
//! scope correction on this work item names it and rules it out of this instrument).
//!
//! `json_parse_scaling_probe`'s end-to-end parse measures whichever quadratic dominates and
//! cannot by itself distinguish "char_at is O(1)" from "the list carrier is O(m^2)". This bin
//! isolates the one thing STRING-INDEX-0 changed: `char_at` no longer rescans `is_ascii` per
//! call, because ascii-ness is now a precomputed fact carried on `RcStr`. On an ASCII input,
//! per-call cost should therefore be flat against BOTH position and string length.
//!
//! NOT floor-enrolled — run standalone via `cargo run --release -p v1-compiler --bin
//! char_at_scaling_probe`. Entry: `dag/gunbc/char_at_scaling_probe_support.dag`'s
//! `char_at_probe(s, pos)`, a one-line wrapper around the `char_at` free call.
//!
//! CHECKABLE RECEIPT: for each (length, position) pair, mean per-call elapsed time over
//! `CHAR_AT_PROBE_REPS` repeated calls through the interpreter — printed as TSV.
//!
//! DISSOLUTION (own trigger, independent of `json_parse_scaling_probe.rs`'s — see
//! `char_at_scaling_probe_dissolution` in `dag/gunbc/char_at_scaling_probe_support.dag`,
//! DESIGN §5's same-unit rule): delete this bin when CHARAT-0's `char_at` O(1) property is
//! floor-enrolled with a modeled witness, or when a fresh run's own printed TSV (the
//! CHECKABLE RECEIPT below) shows `mean_call_us` growing with `string_len` across the
//! `CHAR_AT_PROBE_LENGTHS` range instead of staying flat.
//! Receipt: `rg CHAR_AT_SCALING_PROBE_SCAFFOLD_MARKER src/v1/stage0` until deletion.

/// Grep receipt for scaffold dissolution (`rg CHAR_AT_SCALING_PROBE_SCAFFOLD_MARKER`).
pub const CHAR_AT_SCALING_PROBE_SCAFFOLD_MARKER: &str =
"CHARAT-0 char_at_scaling_probe measurement transport (not floor-enrolled)";

use std::process::ExitCode;
use std::time::Instant;

use v1_compiler::cli_run::{make_eval_context, resolve_entry_graph, workspace_root};
use v1_compiler::v1_interpreter::{self, str_value, ExecutionMode, Value};

const ENTRY: &str = "dag/gunbc/char_at_scaling_probe_support.dag";

/// One repeated call per (length, position) pair; small enough to keep the whole
/// grid under a minute, large enough to average out interpreter dispatch noise.
const DEFAULT_REPS: usize = 2000;

fn reps_from_env() -> usize {
std::env::var("CHAR_AT_PROBE_REPS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_REPS)
}

/// Pure-ASCII fill so the fast path (byte-offset indexing, no `.chars().nth` walk) is
/// what's being timed — this is the branch STRING-INDEX-0 made O(1).
fn make_ascii_string(len: usize) -> String {
(0..len).map(|i| (b'a' + (i % 26) as u8) as char).collect()
}

fn resolve_ctx() -> Result<v1_interpreter::InterpContext, String> {
let ws = workspace_root();
let roots = vec![ws.join("dag").to_string_lossy().into_owned()];
eprintln!("char_at_scaling_probe: resolving {ENTRY} ...");
let resolve_start = Instant::now();
let (graph, indices) = resolve_entry_graph(&roots, ENTRY)?;
eprintln!(
"char_at_scaling_probe: resolve_ms={}",
resolve_start.elapsed().as_millis()
);
Ok(make_eval_context(&graph, indices, ExecutionMode::Hermetic))
}

fn call_char_at_once(
ctx: &v1_interpreter::InterpContext,
s: &Value,
pos: i64,
) -> Result<(), String> {
let args = [
(Some("s".to_string()), s.clone()),
(Some("pos".to_string()), Value::Int(pos)),
];
v1_interpreter::run_in_context_with_args(ctx, "char_at_probe", &args, false)
.map(|_| ())
.map_err(|e| format!("char_at_probe: {e}"))
}

/// Mean per-call elapsed microseconds over `reps` calls at one fixed (s, pos).
fn mean_call_us(
ctx: &v1_interpreter::InterpContext,
s: &Value,
pos: i64,
reps: usize,
) -> Result<f64, String> {
// One untimed warmup call so any first-call setup (e.g. lazy memo frame init)
// doesn't bias the measured mean.
call_char_at_once(ctx, s, pos)?;
let start = Instant::now();
for _ in 0..reps {
call_char_at_once(ctx, s, pos)?;
}
let elapsed = start.elapsed();
Ok(elapsed.as_secs_f64() * 1_000_000.0 / reps as f64)
}

fn run() -> Result<(), String> {
let ctx = resolve_ctx()?;
let reps = reps_from_env();
let lengths: Vec<usize> = std::env::var("CHAR_AT_PROBE_LENGTHS")
.ok()
.map(|s| {
s.split(',')
.filter_map(|tok| tok.trim().parse().ok())
.collect()
})
.filter(|v: &Vec<usize>| !v.is_empty())
.unwrap_or_else(|| vec![10_000, 100_000, 500_000, 2_000_000]);
let position_fractions: [f64; 5] = [0.0, 0.25, 0.5, 0.75, 0.99];

eprintln!("char_at_scaling_probe: reps={reps} lengths={lengths:?}");
println!("mode\tchar_at_scaling\treps={reps}");
println!("string_len\tposition\tposition_frac\tmean_call_us");

for &len in &lengths {
let s = make_ascii_string(len);
let value = str_value(&s);
for frac in position_fractions {
let pos = ((len.saturating_sub(1)) as f64 * frac).round() as i64;
let mean_us = mean_call_us(&ctx, &value, pos, reps)?;
println!("{len}\t{pos}\t{frac:.2}\t{mean_us:.3}");
}
}
Ok(())
}

fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("char_at_scaling_probe: ERROR: {e}");
ExitCode::FAILURE
}
}
}
42 changes: 37 additions & 5 deletions src/v1/stage0/src/bin/json_parse_scaling_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//! json_parse_scaling_probe`. Modes (`JSON_PARSE_PROBE_MODE`):
//! - `survival`: one `JSON_PARSE_TARGET_BYTES`, exactly one `parse_json` call (fresh process).
//! - `memo_receipt`: one target; cold parse + first repeat + average of subsequent memo hits.
//! - `counters`: one target; reports `MutationCounters` (list_push calls vs items_copied) to
//! distinguish an O(log n) push_back from an O(m) `value_to_list_carrier` fallback copy.
//! - `scaling` / `large`: legacy grids (memo-contaminated; not used for acceptance receipts).
//!
//! CHECKABLE RECEIPT: survival mode records Present parse + member count vs process death.
Expand All @@ -24,11 +26,7 @@ use std::process::ExitCode;
use std::time::Instant;

use v1_compiler::cli_run::{make_eval_context, resolve_entry_graph, workspace_root};
use v1_compiler::v1_interpreter::{self, ExecutionMode, Value};

fn str_value(s: impl AsRef<str>) -> Value {
Value::Str(std::rc::Rc::from(s.as_ref()))
}
use v1_compiler::v1_interpreter::{self, str_value, ExecutionMode, Value};

const ENTRY: &str = "dag/extdeps/languages/json/parse.dag";

Expand Down Expand Up @@ -204,6 +202,39 @@ fn survival_succeeded(outcome: &ParseOutcome) -> bool {
matches!(outcome, ParseOutcome::Parsed { .. })
}

/// One target, one parse — reports `MutationCounters` (list_push calls vs items copied)
/// to distinguish an O(log n) push_back from an O(m) `value_to_list_carrier` fallback
/// materialization. Cheap/decisive per eager-koi-458's 2026-08-17 scope correction.
fn run_counters(ctx: &v1_interpreter::InterpContext) -> Result<ParseOutcome, String> {
let target = target_bytes_from_env()?;
let (member_count, json) = json_object_at_least_bytes(target);
eprintln!(
"json_parse_scaling_probe: counters target={target} members={member_count} bytes={}",
json.len()
);
let before = ctx.mutation_counters_snapshot();
let outcome = parse_json_once(ctx, &json, member_count);
let after = ctx.mutation_counters_snapshot();
println!("mode\tcounters\toutcome={}", outcome_label(&outcome));
println!("counter\tcalls\titems_copied");
println!(
"list_push\t{}\t{}",
after.list_push_calls - before.list_push_calls,
after.list_push_items_copied - before.list_push_items_copied
);
println!(
"list_concat\t{}\t{}",
after.list_concat_calls - before.list_concat_calls,
after.list_concat_items_copied - before.list_concat_items_copied
);
println!(
"map_insert\t{}\t{}",
after.map_insert_calls - before.map_insert_calls,
after.map_insert_entries_copied - before.map_insert_entries_copied
);
Ok(outcome)
}

/// One target, one parse — intended for a fresh process per invocation.
fn run_survival(ctx: &v1_interpreter::InterpContext) -> Result<ParseOutcome, String> {
let target = target_bytes_from_env()?;
Expand Down Expand Up @@ -343,6 +374,7 @@ fn run() -> Result<bool, String> {
let mode = std::env::var("JSON_PARSE_PROBE_MODE").unwrap_or_else(|_| "scaling".to_string());
let success = match mode.as_str() {
"survival" => survival_succeeded(&run_survival(&ctx)?),
"counters" => survival_succeeded(&run_counters(&ctx)?),
"memo_receipt" => {
run_memo_receipt(&ctx)?;
true
Expand Down
2 changes: 1 addition & 1 deletion src/v1/stage0/src/derived_realization_schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub fn realize_pack_width_from_scalars(
_ => -1,
};
let verdict = match realize_ctx.field(&fields, "verdict") {
Some(Value::Str(s)) => Rc::clone(s),
Some(Value::Str(s)) => s.rc(),
_ => Rc::from("unknown"),
};
Ok(DerivedScheduleWidth {
Expand Down
2 changes: 1 addition & 1 deletion src/v1/stage0/src/recorded_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ pub fn value_to_fixture_json(
Value::Bool(b) => Ok(json!({ "__tag": "Bool", "value": b })),
Value::Int(n) => Ok(json!({ "__tag": "Int", "value": n })),
Value::Float(f) => Ok(json!({ "__tag": "Float", "value": f })),
Value::Str(s) => Ok(json!({ "__tag": "Str", "value": s })),
Value::Str(s) => Ok(json!({ "__tag": "Str", "value": s.as_ref() })),
Value::List(items) => {
let arr: Result<Vec<_>, _> = items
.iter()
Expand Down
Loading
Loading