Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
17 changes: 16 additions & 1 deletion crates/labcolors-core/src/generic_boundary_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,17 @@ fn assert_only_in_compile_fail(source: &str, needle: &str) {
_ => {}
}

assert!(
!in_compile_fail || doc.is_some() || line.trim().is_empty(),
"compile_fail sentinel was interrupted by live code at line {}",
line_index + 1,
);
let line_occurrences = line.matches(needle).count();
if line_occurrences == 0 {
continue;
}
assert!(
in_compile_fail,
in_compile_fail && doc.is_some(),
"`{needle}` escaped its negative compile_fail sentinel at line {}",
line_index + 1,
);
Expand All @@ -111,6 +116,16 @@ fn assert_only_in_compile_fail(source: &str, needle: &str) {
);
}

#[test]
fn compile_fail_scanner_rejects_live_code_between_document_fences() {
let escaped = "/// ```compile_fail\npub type PackageProgram = u8;\n/// ```";
assert!(
std::panic::catch_unwind(|| assert_only_in_compile_fail(escaped, "PackageProgram"))
.is_err(),
"a live declaration must never inherit compile_fail state from adjacent documentation",
);
}

fn production_rust_sources() -> Vec<(String, String)> {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut pending = vec![root.clone()];
Expand Down
29 changes: 21 additions & 8 deletions crates/labcolors-core/src/joint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@ impl FiniteDomainOrdinalV1 {
/// becomes an implicit tie-break.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AdmittedFiniteJointOrderV1 {
tuples: Box<[Box<[FiniteDomainOrdinalV1]>]>,
first: Box<[FiniteDomainOrdinalV1]>,
rest: Box<[Box<[FiniteDomainOrdinalV1]>]>,
}

impl AdmittedFiniteJointOrderV1 {
pub(crate) fn tuples(&self) -> impl ExactSizeIterator<Item = &[FiniteDomainOrdinalV1]> + '_ {
self.tuples.iter().map(Box::as_ref)
pub(crate) fn tuples(&self) -> impl Iterator<Item = &[FiniteDomainOrdinalV1]> + '_ {
std::iter::once(self.first.as_ref()).chain(self.rest.iter().map(Box::as_ref))
}

pub(crate) fn state_count(&self) -> usize {
// `first` makes the admitted order structurally non-empty; the
// remaining slice length is bounded by Rust's allocation limit.
self.rest.len() + 1
}
}

Expand Down Expand Up @@ -120,10 +127,10 @@ pub(crate) fn admit_finite_joint_order_v1(
.map_err(|_| FiniteJointOrderErrorV1::ResourceExhausted)?;
first_seen.resize(expected, usize::MAX);

let mut tuples = Vec::new();
tuples
.try_reserve_exact(authored.len())
let mut rest = Vec::new();
rest.try_reserve_exact(authored.len() - 1)
.map_err(|_| FiniteJointOrderErrorV1::ResourceExhausted)?;
let mut first_tuple = None;
Comment thread
lemone112 marked this conversation as resolved.
for (tuple_index, tuple) in authored.into_iter().enumerate() {
if tuple.len() != domain_lengths.len() {
return Err(FiniteJointOrderErrorV1::TupleArity {
Expand Down Expand Up @@ -157,11 +164,17 @@ pub(crate) fn admit_finite_joint_order_v1(
});
}
*first = tuple_index;
tuples.push(tuple.into_boxed_slice());
let tuple = tuple.into_boxed_slice();
if first_tuple.is_none() {
first_tuple = Some(tuple);
} else {
rest.push(tuple);
}
}

Ok(AdmittedFiniteJointOrderV1 {
tuples: tuples.into_boxed_slice(),
first: first_tuple.ok_or(FiniteJointOrderErrorV1::EmptyOrder)?,
rest: rest.into_boxed_slice(),
})
}
use crate::session::SessionObservationBindingPermitV1;
Expand Down
51 changes: 51 additions & 0 deletions crates/labcolors-core/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,37 @@ pub struct OwnerV1 {
compiled: CompiledCoreProgramV1,
}

/// Верхние границы числа клеток в новом сертификате одного Observed-update.
///
/// Границы относятся только к текущим клеткам доказательства. Они не включают
/// сохранённый прошлый сертификат, observation/provenance, выходы, операции или
/// байты конкретного транспорта.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EvidenceCellBoundsV1 {
verified_cells: usize,
conflict_cells: usize,
}

impl EvidenceCellBoundsV1 {
/// Максимум клеток успешного сертификата.
pub const fn verified_cells(self) -> usize {
self.verified_cells
}

/// Максимум клеток исчерпывающего конфликтного сертификата.
pub const fn conflict_cells(self) -> usize {
self.conflict_cells
}
}

/// Закрытая причина невозможности вычислить границы сертификата.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvidenceBoundsErrorV1 {
/// Произведение числа сценариев, ограничений и состояний не помещается в
/// адресное пространство платформы.
CardinalityOverflow,
}

/// Отказ доступа из-за несовпадения точной owner-эпохи.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessErrorV1 {
Expand All @@ -1279,6 +1310,26 @@ impl OwnerV1 {
ContentIdentityV1::from_core(self.compiled.content_identity())
}

/// Вычисляет верхние границы клеток для prospective Observed-update.
///
/// `scenario_count` — число объявленных клиентом сценариев до admission.
/// Core сам схлопывает физически одинаковые сценарии, поэтому фактический
/// сертификат может быть короче. Нулевое значение разрешено только как
/// чистый арифметический preflight; пустой Observed-update по-прежнему не
/// допускается. Запрос не создаёт Session и не меняет состояние.
pub fn evidence_cell_bounds(
&self,
scenario_count: usize,
) -> Result<EvidenceCellBoundsV1, EvidenceBoundsErrorV1> {
self.compiled
.evidence_cell_bounds(scenario_count)
.map(|(verified_cells, conflict_cells)| EvidenceCellBoundsV1 {
verified_cells,
conflict_cells,
})
.ok_or(EvidenceBoundsErrorV1::CardinalityOverflow)
}

/// Число значений Surface в каждом schema-ordered сценарии.
pub fn surface_input_port_count(&self) -> usize {
self.compiled.surface_input_ports().len()
Expand Down
71 changes: 52 additions & 19 deletions crates/labcolors-core/src/program_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,12 @@ enum CompiledConstraintModeV1 {
ReportOnly,
}

impl CompiledConstraintModeV1 {
const fn rejects_candidate(self) -> bool {
matches!(self, Self::Hard)
}
}

struct CompiledPointConstraint<Invocation> {
id: ConstraintId,
target_id: OccurrenceId,
Expand Down Expand Up @@ -1127,6 +1133,11 @@ where
self.owner_generation.outputs.len()
}

pub(crate) fn evidence_cell_bounds(&self, scenario_count: usize) -> Option<(usize, usize)> {
checked_program_epoch_evaluation_cell_counts(&self.owner_generation, scenario_count)
.map(|counts| (counts.selected, counts.exhaustive_conflict))
}

pub(crate) fn output_slot_at(&self, index: usize) -> Option<OutputSlotId> {
self.owner_generation
.outputs
Expand Down Expand Up @@ -1268,7 +1279,7 @@ where
}

pub const fn is_hard(&self) -> bool {
matches!(self.mode, CompiledConstraintModeV1::Hard)
self.mode.rejects_candidate()
}

pub const fn result(&self) -> &ProgramConstraintResultV1<Evaluation> {
Expand Down Expand Up @@ -1466,22 +1477,52 @@ fn checked_program_evaluation_cell_counts(
physical_case_count: usize,
constraint_count: usize,
state_count: usize,
can_conflict: bool,
) -> Option<ProgramEvaluationCellCountsV1> {
let selected = physical_case_count.checked_mul(constraint_count)?;
let exhaustive_conflict = selected.checked_mul(state_count)?;
let exhaustive_conflict = if can_conflict {
selected.checked_mul(state_count)?
} else {
0
};
Some(ProgramEvaluationCellCountsV1 {
selected,
exhaustive_conflict,
})
}

fn checked_program_epoch_evaluation_cell_counts<Evaluation>(
epoch: &ProgramEpochV1<Evaluation>,
physical_case_count: usize,
) -> Option<ProgramEvaluationCellCountsV1>
where
Evaluation: ProgramConstraintEvaluatorSetV1,
ProgramConstraintInvocationOf<Evaluation>: Copy,
{
let state_count = epoch
.joint_selection
.as_ref()
.map(|selection| selection.order.state_count())
.unwrap_or(1);
let can_conflict = epoch
.constraints
.iter()
.any(|constraint| constraint.mode.rejects_candidate());
checked_program_evaluation_cell_counts(
physical_case_count,
epoch.constraints.len(),
state_count,
can_conflict,
)
}
Comment thread
lemone112 marked this conversation as resolved.

#[cfg(test)]
pub(crate) fn checked_program_evaluation_cell_counts_for_test(
physical_case_count: usize,
constraint_count: usize,
state_count: usize,
) -> Option<(usize, usize)> {
checked_program_evaluation_cell_counts(physical_case_count, constraint_count, state_count)
checked_program_evaluation_cell_counts(physical_case_count, constraint_count, state_count, true)
.map(|counts| (counts.selected, counts.exhaustive_conflict))
}

Expand Down Expand Up @@ -1585,7 +1626,6 @@ where
fn prepare_program_evaluation_buffers<Evaluation>(
epoch: &ProgramEpochV1<Evaluation>,
observation: &RevisionBoundObservationV1,
joint_state_count: Option<usize>,
) -> Result<
PreparedProgramEvaluationBuffersV1<Evaluation>,
ProgramSessionEvaluationError<ProgramEvaluatorError<Evaluation>>,
Expand All @@ -1594,22 +1634,15 @@ where
Evaluation: ProgramConstraintEvaluatorSetV1,
ProgramConstraintInvocationOf<Evaluation>: Copy,
{
let state_count = joint_state_count.unwrap_or(1);
if state_count == 0 {
return Err(ProgramSessionEvaluationError::InternalInvariant);
}
let counts = checked_program_evaluation_cell_counts(
observation.physical_case_count(),
epoch.constraints.len(),
state_count,
)
.ok_or(ProgramSessionEvaluationError::ResourceExhausted)?;
let counts =
checked_program_epoch_evaluation_cell_counts(epoch, observation.physical_case_count())
.ok_or(ProgramSessionEvaluationError::ResourceExhausted)?;

let mut selected_cells = Vec::new();
try_reserve_program_evaluation_buffer(&mut selected_cells, counts.selected)
.map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?;
let mut conflict_cells = Vec::new();
if joint_state_count.is_some() {
if epoch.joint_selection.is_some() && counts.exhaustive_conflict != 0 {
try_reserve_program_evaluation_buffer(&mut conflict_cells, counts.exhaustive_conflict)
.map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?;
}
Expand Down Expand Up @@ -1687,7 +1720,7 @@ where
ProgramConstraintInvocationOf<Evaluation>: Copy,
{
let Some(selection) = &epoch.joint_selection else {
let mut buffers = prepare_program_evaluation_buffers(epoch, &observation, None)?;
let mut buffers = prepare_program_evaluation_buffers(epoch, &observation)?;
return collect_program_candidate_into(
plan,
epoch,
Expand All @@ -1698,8 +1731,8 @@ where
);
};

let state_count = selection.order.tuples().len();
let mut buffers = prepare_program_evaluation_buffers(epoch, &observation, Some(state_count))?;
let state_count = selection.order.state_count();
let mut buffers = prepare_program_evaluation_buffers(epoch, &observation)?;
for (state_index, tuple) in selection.order.tuples().enumerate() {
apply_joint_candidate(plan, &epoch.finite_targets, tuple)?;
if !scan_program_candidate(plan, epoch, &observation, state_index, None, None)? {
Expand Down Expand Up @@ -1972,7 +2005,7 @@ where
let result = match decision {
HardDecision::Pass(evidence) => ProgramConstraintResultV1::Pass(evidence),
HardDecision::Violation(evidence) => {
if matches!(constraint.mode, CompiledConstraintModeV1::Hard) {
if constraint.mode.rejects_candidate() {
has_hard_violation = true;
}
ProgramConstraintResultV1::Violation(evidence)
Expand Down
Loading
Loading