#[cfg(any(test, feature = "test-support"))]
use crate::kernel_header::RefWidth;
use crate::nurbs::reader::{checked_knot_layout, BsplineMarker, Nullable};
use crate::sab::Token;
#[derive(Clone, Copy)]
pub struct Cur<'a> {
toks: &'a [Token],
pos: usize,
}
impl<'a> Cur<'a> {
pub fn at(toks: &'a [Token], pos: usize) -> Self {
Self { toks, pos }
}
pub fn pos(&self) -> usize {
self.pos
}
pub(super) fn set_pos(&mut self, pos: usize) {
self.pos = pos;
}
pub(super) fn toks(&self) -> &'a [Token] {
self.toks
}
pub(super) fn peek(&self) -> Option<&'a Token> {
self.toks.get(self.pos)
}
pub(super) fn at_scope_end(&self) -> bool {
self.pos + 1 == self.toks.len()
&& matches!(self.toks.get(self.pos), Some(Token::SubtypeClose))
}
pub(super) fn bump(&mut self) -> Option<&'a Token> {
let token = self.toks.get(self.pos)?;
self.pos += 1;
Some(token)
}
pub(super) fn rest(&self) -> &'a [Token] {
self.toks.get(self.pos..).unwrap_or(&[])
}
pub(super) fn take_f64(&mut self) -> Option<f64> {
match self.peek()? {
Token::Double(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_long(&mut self) -> Option<i64> {
match self.peek()? {
Token::Long(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_enum(&mut self) -> Option<i64> {
match self.peek()? {
Token::Enum(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_bool(&mut self) -> Option<bool> {
match self.peek()? {
Token::True => {
self.pos += 1;
Some(true)
}
Token::False => {
self.pos += 1;
Some(false)
}
_ => None,
}
}
pub(super) fn take_str(&mut self) -> Option<&'a str> {
match self.peek()? {
Token::Str(value) => {
self.pos += 1;
Some(value)
}
_ => None,
}
}
pub(super) fn take_ident(&mut self) -> Option<&'a str> {
match self.peek()? {
Token::Ident(value) | Token::SubIdent(value) => {
self.pos += 1;
Some(value)
}
_ => None,
}
}
pub(super) fn take_position(&mut self) -> Option<[f64; 3]> {
match self.peek()? {
Token::Position(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_vector3(&mut self) -> Option<[f64; 3]> {
match self.peek()? {
Token::Vector3(value) => {
self.pos += 1;
Some(*value)
}
_ => None,
}
}
pub(super) fn take_float_array(&mut self) -> Option<Vec<f64>> {
let mark = self.pos;
let Some(count) = self.take_long().and_then(|c| usize::try_from(c).ok()) else {
self.pos = mark;
return None;
};
let mut values = Vec::new();
for _ in 0..count {
let Some(value) = self.take_f64() else {
self.pos = mark;
return None;
};
values.push(value);
}
Some(values)
}
pub(super) fn take_range_value(&mut self) -> Option<f64> {
let mark = self.pos;
if matches!(self.peek(), Some(Token::True | Token::False)) {
self.pos += 1;
}
let Some(value) = self.take_f64() else {
self.pos = mark;
return None;
};
Some(value)
}
pub(super) fn take_optional_range_value(&mut self) -> Option<Nullable<f64>> {
let mark = self.pos;
match self.peek()? {
Token::True => {
self.pos += 1;
let Some(value) = self.take_f64() else {
self.pos = mark;
return None;
};
Some(Nullable::Value(value))
}
Token::False => {
self.pos += 1;
Some(Nullable::Null)
}
Token::Double(_) => self.take_f64().map(Nullable::Value),
_ => None,
}
}
}
pub(super) fn take_knot_table(
cur: &mut Cur<'_>,
n: usize,
degree: i64,
) -> Option<(Vec<f64>, usize)> {
if n > cur.rest().len() / 2 {
return None;
}
let mut values = Vec::new();
let mut mults = Vec::new();
for _ in 0..n {
values.push(cur.take_f64()?);
mults.push(cur.take_long()?);
}
let expansion = checked_knot_layout(&mults, degree)?;
let mut expanded = Vec::new();
for (index, (value, multiplicity)) in values.iter().zip(&mults).enumerate() {
let run_length = usize::try_from(*multiplicity).ok()?
+ usize::from(index == 0 || index + 1 == mults.len());
for _ in 0..run_length {
expanded.push(*value);
}
}
Some((expanded, expansion.n_poles))
}
pub(super) fn marker_at(toks: &[Token], pos: usize) -> Option<BsplineMarker> {
match toks.get(pos)? {
Token::Ident(name) if name == "nubs" => Some(BsplineMarker::Nubs),
Token::Ident(name) if name == "nurbs" => Some(BsplineMarker::Nurbs),
_ => None,
}
}
pub(super) fn owned_marker_positions(toks: &[Token]) -> Option<Vec<usize>> {
let (out, balanced) = walk_owned_markers(toks);
balanced.then_some(out)
}
fn walk_owned_markers(toks: &[Token]) -> (Vec<usize>, bool) {
let mut out = Vec::new();
let mut depth = 0usize;
let mut outer = usize::from(matches!(toks.first(), Some(Token::SubtypeOpen)));
for (pos, token) in toks.iter().enumerate().skip(outer) {
match token {
Token::SubtypeOpen => depth += 1,
Token::SubtypeClose => match depth.checked_sub(1) {
Some(next) => depth = next,
None => match outer.checked_sub(1) {
Some(next) => outer = next,
None => return (out, false),
},
},
_ => {
if depth == 0 && marker_at(toks, pos).is_some() {
out.push(pos);
}
}
}
}
(out, true)
}
pub(super) fn owned_subtype_defs(toks: &[Token]) -> Option<Vec<(usize, &str)>> {
let mut owned = Vec::new();
let mut depth = 0usize;
for (pos, token) in toks.iter().enumerate() {
match token {
Token::SubtypeOpen => {
if depth == 0 {
if let Some(Token::Ident(name) | Token::SubIdent(name)) = toks.get(pos + 1) {
owned.push((pos, name.as_str()));
}
}
depth += 1;
}
Token::SubtypeClose => depth = depth.checked_sub(1)?,
_ => {}
}
}
Some(owned)
}
pub(super) fn find_owned_subtype_marker<'n>(
toks: &[Token],
names: &[&'n str],
) -> Option<(usize, &'n str)> {
let owned = owned_subtype_defs(toks)?;
names.iter().copied().find_map(|name| {
owned
.iter()
.find(|(_, owned_name)| *owned_name == name)
.map(|(start, _)| (*start, name))
})
}
pub fn owned_construction_subtype(toks: &[Token]) -> Option<String> {
owned_subtype_defs(toks)?
.into_iter()
.map(|(_, name)| name)
.find(|name| *name != "ref")
.map(|name| canonical_intcurve_kind(name).to_owned())
}
pub(super) fn cache_scope(toks: &[Token]) -> Option<&[Token]> {
let mut constructions = 0usize;
let mut cache_bearing = Vec::new();
let owned = owned_subtype_defs(toks)?;
for (start, _) in owned.into_iter().filter(|(_, name)| *name != "ref") {
constructions += 1;
let Some(scope) = subtype_span(toks, start) else {
continue;
};
if !scope.owned_marker_positions().is_empty() {
cache_bearing.push(scope.tokens());
}
}
match (cache_bearing.as_slice(), constructions) {
([scope], _) => Some(scope),
([], 0) => Some(toks),
_ => None,
}
}
fn canonical_intcurve_kind(name: &str) -> &str {
super::subtypes::INTCURVE_ALIASES
.iter()
.find_map(|(modern, legacy)| (*legacy == name).then_some(*modern))
.unwrap_or(name)
}
pub(super) fn find_owned_intcurve_subtype(toks: &[Token], modern: &str) -> Option<usize> {
if modern.is_empty() {
return None;
}
let legacy = super::subtypes::INTCURVE_ALIASES
.iter()
.find_map(|(name, alias)| (*name == modern).then_some(*alias));
let found = match legacy {
Some(legacy) => find_owned_subtype_marker(toks, &[modern, legacy]),
None => find_owned_subtype_marker(toks, &[modern]),
};
found.map(|(marker, _)| marker)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SubtypeScope<'a> {
tokens: &'a [Token],
}
impl<'a> SubtypeScope<'a> {
pub fn tokens(&self) -> &'a [Token] {
self.tokens
}
pub fn interior(&self) -> &'a [Token] {
&self.tokens[1..self.tokens.len() - 1]
}
pub fn owned_marker_positions(&self) -> Vec<usize> {
walk_owned_markers(self.tokens).0
}
}
pub(super) fn subtype_span(toks: &[Token], start: usize) -> Option<SubtypeScope<'_>> {
matches!(toks.get(start), Some(Token::SubtypeOpen)).then_some(())?;
let mut depth = 0usize;
for (pos, token) in toks.iter().enumerate().skip(start) {
match token {
Token::SubtypeOpen => depth += 1,
Token::SubtypeClose => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(SubtypeScope {
tokens: toks.get(start..=pos)?,
});
}
}
_ => {}
}
}
None
}
pub(super) fn subtype_refs(toks: &[Token]) -> impl Iterator<Item = usize> + '_ {
toks.iter().enumerate().filter_map(|(pos, token)| {
if !matches!(token, Token::SubtypeOpen) {
return None;
}
match (toks.get(pos + 1), toks.get(pos + 2)) {
(Some(Token::Ident(name)), Some(Token::Long(index)))
if name == "ref" && *index >= 0 =>
{
usize::try_from(*index).ok()
}
(Some(Token::Long(index)), Some(Token::SubtypeClose)) if *index >= 0 => {
usize::try_from(*index).ok()
}
_ => None,
}
})
}
pub fn payload_subtype_toks<'r>(
record: &'r crate::sab::Record,
chunk_index: usize,
expected: &str,
) -> Option<SubtypeScope<'r>> {
let mut chunk = 0usize;
let mut open = None;
for (pos, token) in record.tokens.iter().enumerate() {
if token.is_payload_ident() {
continue;
}
if chunk == chunk_index {
open = Some(pos);
break;
}
chunk += 1;
}
let open = open?;
if !matches!(record.tokens.get(open), Some(Token::SubtypeOpen)) {
return None;
}
let (Token::Ident(name) | Token::SubIdent(name)) = record.tokens.get(open + 1)? else {
return None;
};
if name != expected {
return None;
}
subtype_span(&record.tokens, open)
}
pub struct SubtypeTable {
defs: Vec<(std::sync::Arc<[Token]>, usize)>,
save_format_version: Option<u32>,
}
impl SubtypeTable {
pub fn from_records(records: &[crate::sab::Record]) -> Self {
let mut defs = Vec::new();
for record in records {
for (pos, token) in record.tokens.iter().enumerate() {
if matches!(token, Token::SubtypeOpen) {
if let Some(Token::Ident(name) | Token::SubIdent(name)) =
record.tokens.get(pos + 1)
{
if name != "ref" {
defs.push((record.tokens.clone(), pos));
}
}
}
}
}
Self {
defs,
save_format_version: None,
}
}
pub fn from_definitions(defs: Vec<(std::sync::Arc<[Token]>, usize)>) -> Self {
Self {
defs,
save_format_version: None,
}
}
#[must_use]
pub fn with_save_format_version(mut self, version: Option<u32>) -> Self {
self.save_format_version = version;
self
}
pub(super) fn save_format_version(&self) -> Option<u32> {
self.save_format_version
}
pub(super) fn span(&self, index: usize) -> Option<SubtypeScope<'_>> {
let (tokens, token_pos) = self.defs.get(index)?;
subtype_span(tokens, *token_pos)
}
}
pub(crate) fn admit_subtype_references(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
records: &[crate::sab::Record],
table: &SubtypeTable,
) -> Result<(), cadmpeg_core::CodecError> {
let mut reach = ReferenceReach::new(table);
let mut guards = Vec::new();
for record in records {
for index in subtype_refs(&record.tokens) {
let depth = reach.of(index);
while guards.len() < depth {
guards.push(ctx.enter_nested("follow ASM subtype reference")?);
}
}
}
Ok(())
}
struct ReferenceReach<'t> {
table: &'t SubtypeTable,
order: Vec<Option<usize>>,
low: Vec<usize>,
on_stack: Vec<bool>,
component: Vec<Option<usize>>,
edges: Vec<Vec<usize>>,
followed: Vec<bool>,
reach: Vec<usize>,
next_order: usize,
}
impl<'t> ReferenceReach<'t> {
fn new(table: &'t SubtypeTable) -> Self {
Self {
table,
order: vec![None; table.defs.len()],
low: vec![0; table.defs.len()],
on_stack: vec![false; table.defs.len()],
component: vec![None; table.defs.len()],
edges: vec![Vec::new(); table.defs.len()],
followed: vec![false; table.defs.len()],
reach: Vec::new(),
next_order: 0,
}
}
fn of(&mut self, index: usize) -> usize {
if index >= self.order.len() {
return 0;
}
if self.order[index].is_none() {
self.explore(index);
}
self.component[index].map_or(0, |component| self.reach[component])
}
fn visit(&mut self, index: usize, stack: &mut Vec<usize>) {
self.order[index] = Some(self.next_order);
self.low[index] = self.next_order;
self.next_order += 1;
stack.push(index);
self.on_stack[index] = true;
if let Some(scope) = self.table.span(index) {
self.followed[index] = true;
self.edges[index] = subtype_refs(scope.tokens())
.filter(|&target| target < self.order.len())
.collect();
}
}
fn explore(&mut self, root: usize) {
let mut stack = Vec::new();
self.visit(root, &mut stack);
let mut frames = vec![(root, 0usize)];
while let Some(&mut (index, ref mut position)) = frames.last_mut() {
if let Some(&target) = self.edges[index].get(*position) {
*position += 1;
match self.order[target] {
None => {
self.visit(target, &mut stack);
frames.push((target, 0));
}
Some(order) if self.on_stack[target] => {
self.low[index] = self.low[index].min(order);
}
Some(_) => {}
}
continue;
}
frames.pop();
if let Some(&(parent, _)) = frames.last() {
self.low[parent] = self.low[parent].min(self.low[index]);
}
if Some(self.low[index]) == self.order[index] {
self.close_component(index, &mut stack);
}
}
}
fn close_component(&mut self, root: usize, stack: &mut Vec<usize>) {
let component = self.reach.len();
let mut members = Vec::new();
while let Some(member) = stack.pop() {
self.on_stack[member] = false;
self.component[member] = Some(component);
members.push(member);
if member == root {
break;
}
}
let mut below = 0;
let mut followed = 0;
for &member in &members {
followed += usize::from(self.followed[member]);
for &target in &self.edges[member] {
if let Some(other) = self.component[target].filter(|&other| other != component) {
below = below.max(self.reach[other]);
}
}
}
self.reach.push(followed + below);
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn lex_test_span(
bytes: &[u8],
ref_width: RefWidth,
) -> Result<std::sync::Arc<[Token]>, crate::stream_error::StreamError> {
let mut wrapped = vec![0x0d, 1, b'x'];
wrapped.extend_from_slice(bytes);
wrapped.push(0x11);
let records = crate::test_support::sab::frame(&wrapped, 0, wrapped.len(), ref_width)?;
let [record]: [crate::sab::Record; 1] =
records
.try_into()
.map_err(|records: Vec<_>| crate::stream_error::StreamError {
format: crate::stream_error::StreamFormat::Binary,
offset: 0,
reason: format!(
"bare payload frames as {} records, expected one",
records.len()
),
})?;
Ok(record.tokens)
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_table(
bytes: &[u8],
ref_width: RefWidth,
) -> Result<SubtypeTable, crate::stream_error::StreamError> {
let record = crate::sab::Record {
index: 0,
name: String::new(),
tokens: lex_test_span(bytes, ref_width)?,
offset: 0,
len: 0,
};
Ok(SubtypeTable::from_records(&[record]))
}
#[cfg(test)]
mod tests {
use super::{
cache_scope, lex_test_span, marker_at, owned_construction_subtype, owned_marker_positions,
owned_subtype_defs, subtype_refs, subtype_span, test_table, Cur,
};
use crate::kernel_header::RefWidth;
use crate::nurbs::reader::BsplineMarker;
use crate::sab::Token;
#[test]
fn subtype_reference_walk_refuses_depth_before_following_next_definition() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let records: Vec<crate::sab::Record> = (0..3)
.map(|index| {
let mut tokens = vec![Token::SubtypeOpen, Token::Ident("node".into())];
if index < 2 {
tokens.extend([
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(index + 1),
Token::SubtypeClose,
]);
}
tokens.push(Token::SubtypeClose);
crate::sab::Record {
index: usize::try_from(index).expect("test value fits"),
name: "node".into(),
tokens: tokens.into(),
offset: 0,
len: 0,
}
})
.collect();
let table = super::SubtypeTable::from_records(&records);
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[0], &arena, &policy).unwrap();
let error = super::admit_subtype_references(&ctx, &records, &table)
.expect_err("second followed reference exceeds depth one");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal, got {error:?}");
};
assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
assert_eq!(limit.operation, "follow ASM subtype reference");
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[0], &arena, &policy).unwrap();
super::admit_subtype_references(&ctx, &records, &table)
.expect("admit the finite subtype reference chain");
}
fn reference_record(index: usize, tokens: Vec<Token>) -> crate::sab::Record {
crate::sab::Record {
index,
name: "node".into(),
tokens: tokens.into(),
offset: 0,
len: 0,
}
}
fn reference(index: i64) -> [Token; 4] {
[
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(index),
Token::SubtypeClose,
]
}
#[test]
fn many_records_sharing_one_large_definition_admit_in_linear_time() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
const RECORDS: usize = 1 << 16;
const TOKENS: i64 = 1 << 16;
let mut definition = vec![Token::SubtypeOpen, Token::Ident("large".into())];
definition.extend((0..TOKENS).map(Token::Long));
definition.push(Token::SubtypeClose);
let mut records = vec![reference_record(0, definition)];
records.extend((1..=RECORDS).map(|index| reference_record(index, reference(0).to_vec())));
let table = super::SubtypeTable::from_records(&records);
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[0], &arena, &policy).unwrap();
super::admit_subtype_references(&ctx, &records, &table).unwrap();
assert!(ctx.finish_session().is_ok());
}
#[test]
fn a_reference_cycle_holds_each_of_its_definitions_once() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
let node = |target| {
let mut tokens = vec![Token::SubtypeOpen, Token::Ident("node".into())];
tokens.extend(reference(target));
tokens.push(Token::SubtypeClose);
tokens
};
let records = vec![
reference_record(0, node(1)),
reference_record(1, node(0)),
reference_record(2, reference(1).to_vec()),
];
let table = super::SubtypeTable::from_records(&records);
for (depth, admitted) in [(1, false), (2, true)] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = depth;
let (ctx, _) = DecodeContext::from_root_bytes(&[0], &arena, &policy).unwrap();
assert_eq!(
super::admit_subtype_references(&ctx, &records, &table).is_ok(),
admitted
);
}
}
#[test]
fn supplied_definitions_preserve_global_order_and_admit_external_references() {
use crate::sab::Record;
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
let payload: std::sync::Arc<[Token]> = vec![
Token::SubtypeOpen,
Token::Ident("first".into()),
Token::SubtypeClose,
Token::SubtypeOpen,
Token::Ident("second".into()),
Token::SubtypeClose,
]
.into();
let table = super::SubtypeTable::from_definitions(vec![(payload.clone(), 3), (payload, 0)])
.with_save_format_version(Some(23100));
assert_eq!(
table.span(0).unwrap().interior(),
&[Token::Ident("second".into())]
);
assert_eq!(
table.span(1).unwrap().interior(),
&[Token::Ident("first".into())]
);
assert_eq!(table.save_format_version(), Some(23100));
let subset = [Record {
index: 8,
name: "unused".into(),
offset: 0,
len: 0,
tokens: vec![
Token::SubtypeOpen,
Token::Ident("ref".into()),
Token::Long(1),
Token::SubtypeClose,
]
.into(),
}];
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
assert!(super::admit_subtype_references(&ctx, &subset, &table).is_err());
assert!(super::admit_subtype_references(
&ctx,
&subset,
&super::SubtypeTable::from_records(&subset)
)
.is_ok());
}
fn ident(name: &str) -> Token {
Token::Ident(name.to_string())
}
#[test]
fn bare_payload_admission_preserves_empty_and_single_token_payloads() {
for width in [RefWidth::Four, RefWidth::Eight] {
assert!(lex_test_span(&[], width).unwrap().is_empty());
assert_eq!(&*lex_test_span(&[0x0a], width).unwrap(), &[Token::True]);
}
}
#[test]
fn bare_payload_admission_refuses_truncated_tokens() {
for width in [RefWidth::Four, RefWidth::Eight] {
assert!(lex_test_span(&[0x06], width).is_err());
assert!(test_table(&[0x06], width).is_err());
}
}
#[test]
fn bare_payload_admission_refuses_an_extra_record() {
for width in [RefWidth::Four, RefWidth::Eight] {
let bytes = [0x11, 0x0d, 1, b'y'];
let error = lex_test_span(&bytes, width).unwrap_err();
assert!(error.reason.contains("2 records"), "{error}");
assert!(test_table(&bytes, width).is_err());
}
}
#[test]
fn cursor_take_methods_do_not_advance_on_mismatch() {
let toks = [Token::Double(2.5), Token::True, Token::Long(7)];
let mut cur = Cur::at(&toks, 0);
assert_eq!(cur.take_long(), None);
assert_eq!(cur.take_f64(), Some(2.5));
assert_eq!(cur.take_bool(), Some(true));
assert_eq!(cur.take_long(), Some(7));
assert_eq!(cur.bump(), None);
}
#[test]
fn float_array_restores_position_on_a_truncated_body() {
let toks = [Token::Long(2), Token::Double(1.0), Token::True];
let mut cur = Cur::at(&toks, 0);
assert_eq!(cur.take_float_array(), None);
assert_eq!(cur.pos(), 0);
}
#[test]
fn scope_end_requires_the_terminal_close() {
let toks = [Token::SubtypeOpen, ident("x"), Token::SubtypeClose];
assert!(Cur::at(&toks, 2).at_scope_end());
assert!(!Cur::at(&toks, 1).at_scope_end());
let trailing = [
Token::SubtypeOpen,
ident("x"),
Token::SubtypeClose,
Token::Double(1.0),
];
assert!(!Cur::at(&trailing, 2).at_scope_end());
}
#[test]
fn owned_defs_skip_nested_constructions() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeOpen,
ident("ref"),
Token::Long(3),
Token::SubtypeClose,
Token::SubtypeClose,
];
assert_eq!(owned_subtype_defs(&toks), Some(vec![(0, "exactcur")]));
assert_eq!(subtype_refs(&toks).collect::<Vec<_>>(), vec![3]);
assert_eq!(
owned_construction_subtype(&toks),
Some("exact_int_cur".to_string())
);
assert_eq!(
subtype_span(&toks, 2).map(|scope| scope.tokens()),
Some(&toks[2..=5])
);
}
#[test]
fn owned_markers_ignore_nested_scopes_and_a_leading_open() {
let toks = [
Token::SubtypeOpen,
ident("nubs"),
Token::SubtypeOpen,
ident("nurbs"),
Token::SubtypeClose,
];
assert_eq!(owned_marker_positions(&toks), Some(vec![1]));
assert_eq!(marker_at(&toks, 1), Some(BsplineMarker::Nubs));
assert_eq!(marker_at(&toks, 3), Some(BsplineMarker::Nurbs));
}
#[test]
fn a_scope_yields_its_owned_markers_with_no_refusal_to_answer() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeOpen,
ident("ref"),
Token::SubtypeOpen,
ident("nurbs"),
Token::SubtypeClose,
Token::SubtypeClose,
ident("nurbs"),
Token::SubtypeClose,
];
let scope = subtype_span(&toks, 0).expect("balanced scope");
let owned: Vec<usize> = scope.owned_marker_positions();
assert_eq!(owned, vec![2, 9]);
assert_eq!(scope.tokens(), &toks[..]);
}
#[test]
fn a_span_that_does_not_open_at_start_is_not_a_scope() {
let toks = [
ident("x"),
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeClose,
];
assert_eq!(subtype_span(&toks, 0), None);
assert_eq!(subtype_span(&toks, 2), None);
assert_eq!(subtype_span(&toks, 3), None);
assert_eq!(subtype_span(&toks, 4), None);
let scope = subtype_span(&toks, 1).expect("balanced scope");
assert_eq!(scope.tokens(), &toks[1..=3]);
assert_eq!(scope.interior(), &toks[2..3]);
}
#[test]
fn a_first_nested_scope_does_not_own_the_outer_scopes_markers() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeOpen,
ident("support"),
ident("nubs"),
Token::SubtypeClose,
ident("nurbs"),
Token::SubtypeClose,
];
let scope = subtype_span(&toks, 0).expect("balanced scope");
assert_eq!(scope.owned_marker_positions(), vec![6]);
assert_eq!(scope.interior(), &toks[1..7]);
assert_eq!(owned_marker_positions(&toks[2..7]), Some(vec![2, 4]));
}
#[test]
fn one_cache_bearing_scope_is_the_cache_scope_and_two_are_ambiguous() {
let one = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
Token::SubtypeOpen,
ident("ref"),
Token::Long(3),
Token::SubtypeClose,
];
assert_eq!(cache_scope(&one), Some(&one[0..=3]));
let two = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
Token::SubtypeOpen,
ident("exactcur"),
ident("nurbs"),
Token::SubtypeClose,
];
assert_eq!(cache_scope(&two), None);
let bare = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeClose,
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
];
assert_eq!(cache_scope(&bare), Some(&bare[3..=6]));
}
#[test]
fn an_unbalanced_close_refuses_the_cache_scope_rather_than_moving_to_another() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
ident("nubs"),
Token::SubtypeClose,
Token::SubtypeClose,
Token::SubtypeOpen,
ident("exactcur"),
ident("nurbs"),
Token::SubtypeClose,
];
assert_eq!(owned_subtype_defs(&toks), None);
assert_eq!(cache_scope(&toks), None);
}
#[test]
fn one_unbalanced_close_refuses_the_owned_walks() {
let toks = [
Token::SubtypeOpen,
ident("exactcur"),
Token::SubtypeOpen,
ident("ref"),
Token::SubtypeClose,
Token::SubtypeClose,
Token::SubtypeClose,
ident("nurbs"),
];
assert_eq!(owned_marker_positions(&toks), None);
assert_eq!(owned_subtype_defs(&toks), None);
}
#[test]
fn subtype_table_walks_wide_strings_at_the_stream_ref_width() {
fn t_ident(b: &mut Vec<u8>, s: &str) {
b.push(0x0d);
b.push(u8::try_from(s.len()).expect("test value fits"));
b.extend_from_slice(s.as_bytes());
}
for ref_width in [
crate::kernel_header::RefWidth::Four,
crate::kernel_header::RefWidth::Eight,
] {
let payload = [b'0', b'1', b'2', b'3', 0x0f, 0x0d, 0x01, b'x'];
let mut active = Vec::new();
t_ident(&mut active, "tspl");
active.push(0x09);
active.extend_from_slice(&payload.len().to_le_bytes()[..ref_width.bytes()]);
active.extend_from_slice(&payload);
active.extend_from_slice(b"\x0f\x0d\x08real_def\x10");
active.push(0x11);
let records = crate::test_support::sab::frame(&active, 0, active.len(), ref_width)
.expect("wide-string record frames at its declared width");
let table = super::SubtypeTable::from_records(&records);
assert_eq!(table.defs.len(), 1);
assert!(matches!(table.span(0).expect("real definition").tokens(),
[Token::SubtypeOpen, Token::Ident(name), Token::SubtypeClose] if name == "real_def"));
}
}
}