use super::*;
pub(super) fn check_source_nesting(
source: &str,
budget: &mut crate::budget::Budget<'_>,
) -> Result<(), CompileError> {
let limits = budget.limits().clone();
let text = candid_text(source).0;
if let Err(observed) = comment_run_preflight(text.as_bytes(), limits.max_source_nesting) {
return Err(CompileError::resource_limit(
"source_nesting",
limits.max_source_nesting,
observed,
format!(
"Candid source has a run of {observed} consecutive comments, over the source nesting limit {}; a /* */ block counts as one comment",
limits.max_source_nesting
),
));
}
let mut delimiters = 0usize;
let mut unary = 0usize;
for token in Tokenizer::new(text) {
budget
.checkpoint()
.map_err(|error| budget_error(error, DiagnosticPhase::Parse, "source preflight"))?;
let (_, token, _) = match token {
Ok(token) => token,
Err(_) => return Ok(()),
};
match token {
Token::Opt | Token::Vec => unary = unary.saturating_add(1),
Token::LParen | Token::LBrace => {
delimiters = delimiters.saturating_add(1);
unary = 0;
}
Token::RParen | Token::RBrace => {
delimiters = delimiters.saturating_sub(1);
unary = 0;
}
_ => unary = 0,
}
let observed = delimiters.saturating_add(unary);
if observed > limits.max_source_nesting {
return Err(CompileError::resource_limit(
"source_nesting",
limits.max_source_nesting,
observed,
format!(
"Candid source nesting {observed} exceeds limit {}",
limits.max_source_nesting
),
));
}
}
Ok(())
}
pub(super) fn refuse_record_field_id_overflow(
text: &str,
offset: usize,
source_name: Option<&str>,
budget: &crate::budget::Budget<'_>,
) -> Result<(), CompileError> {
enum Frame {
Record {
next_id: u64,
at_field_start: bool,
pending_label: Option<(Option<u64>, std::ops::Range<usize>)>,
overflow: Option<(u64, std::ops::Range<usize>)>,
},
Other,
}
let mut frames: Vec<Frame> = Vec::new();
let mut previous_was_record = false;
for token in Tokenizer::new(text) {
budget
.checkpoint()
.map_err(|error| budget_error(error, DiagnosticPhase::Parse, "source preflight"))?;
let (start, token, end) = match token {
Ok(token) => token,
Err(_) => return Ok(()),
};
if let Some(Frame::Record {
next_id,
pending_label,
overflow,
..
}) = frames.last_mut()
{
if let Some((label_id, range)) = pending_label.take() {
let id = match (token == Token::Colon, label_id) {
(true, Some(id)) => Some(id),
(true, None) => None,
(false, _) => Some(*next_id),
};
if let Some(id) = id {
if id == u64::from(u32::MAX) && overflow.is_none() {
*overflow = Some((id, range));
}
*next_id = id.saturating_add(1);
}
}
}
if let Some(Frame::Record {
at_field_start,
pending_label,
next_id,
overflow,
}) = frames.last_mut()
{
if *at_field_start && !matches!(token, Token::Semi | Token::RBrace) {
*at_field_start = false;
let label_id = match &token {
Token::Decimal(digits) => Some(digits.parse::<u32>().ok().map(u64::from)),
Token::Hex(digits) => Some(u32::from_str_radix(digits, 16).ok().map(u64::from)),
Token::Id(name) | Token::Text(name) => {
Some(Some(u64::from(crate::name_hash::candid_name_hash(name))))
}
_ => None,
};
match label_id {
Some(label_id) => *pending_label = Some((label_id, start..end)),
None => {
let id = *next_id;
if id == u64::from(u32::MAX) && overflow.is_none() {
*overflow = Some((id, start..end));
}
*next_id = id.saturating_add(1);
}
}
}
}
match token {
Token::LBrace if previous_was_record => frames.push(Frame::Record {
next_id: 0,
at_field_start: true,
pending_label: None,
overflow: None,
}),
Token::LBrace | Token::LParen => frames.push(Frame::Other),
Token::RBrace | Token::RParen => {
if let Some(Frame::Record {
overflow: Some((id, range)),
..
}) = frames.pop()
{
let start = range.start.saturating_add(offset);
let end = range.end.saturating_add(offset);
let mut diagnostic = Diagnostic::compiler(
"did_parse_error",
DiagnosticPhase::Parse,
format!(
"Candid parser error: record field id {id} is not supported at bytes {start}..{end}; \
the pinned candid_parser cannot number a record field after it"
),
);
diagnostic.span = Some(SourceSpan::exact(
source_name.map(str::to_string),
start as u64,
end as u64,
));
return Err(CompileError {
diagnostics: vec![diagnostic],
});
}
}
Token::Semi => {
if let Some(Frame::Record { at_field_start, .. }) = frames.last_mut() {
*at_field_start = true;
}
}
_ => {}
}
previous_was_record = token == Token::Record;
}
Ok(())
}
pub(super) fn comment_run_preflight(text: &[u8], limit: usize) -> Result<usize, usize> {
let mut index = 0;
let mut run = 0usize;
let mut longest = 0usize;
while let Some(&byte) = text.get(index) {
match (byte, text.get(index + 1)) {
(b' ' | b'\t' | b'\r' | b'\n', _) => index += 1,
(b'/', Some(b'/' | b'*')) => {
run += 1;
if run > limit {
return Err(run);
}
longest = longest.max(run);
let block = text[index + 1] == b'*';
index += 2;
if block {
let mut depth = 1usize;
while let Some(&byte) = text.get(index) {
match (byte, text.get(index + 1)) {
(b'/', Some(b'*')) => {
depth += 1;
index += 2;
}
(b'*', Some(b'/')) => {
depth -= 1;
index += 2;
if depth == 0 {
break;
}
}
_ => index += 1,
}
}
} else {
index = text[index..]
.iter()
.position(|&byte| byte == b'\n')
.map_or(text.len(), |offset| index + offset);
}
}
(b'"', _) => {
run = 0;
index += 1;
while let Some(&byte) = text.get(index) {
match byte {
b'\\' => index += 2,
b'"' => {
index += 1;
break;
}
_ => index += 1,
}
}
}
_ => {
run = 0;
index += 1;
}
}
}
Ok(longest)
}
pub(super) fn charge_type_preflight(
budget: &mut crate::budget::Budget<'_>,
limit: usize,
amount: usize,
phase: DiagnosticPhase,
operation: &str,
) -> Result<(), CompileError> {
budget
.charge("type_preflight_work", limit, amount)
.map(|_| ())
.map_err(|error| budget_error(error, phase, operation))
}
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct ActiveNames(std::rc::Rc<BTreeSet<usize>>);
impl ActiveNames {
pub(super) fn contains(&self, index: &usize) -> bool {
self.0.contains(index)
}
pub(super) fn len(&self) -> usize {
self.0.len()
}
pub(super) fn with(&self, index: usize) -> Self {
let mut next = (*self.0).clone();
next.insert(index);
Self(std::rc::Rc::new(next))
}
}
pub(super) fn cyclic_nodes(node_count: usize, edges: &[(usize, usize)]) -> Vec<bool> {
let mut forward = vec![Vec::new(); node_count];
let mut reverse = vec![Vec::new(); node_count];
let mut cyclic = vec![false; node_count];
for &(from, to) in edges {
if from == to {
cyclic[from] = true;
} else {
forward[from].push(to);
reverse[to].push(from);
}
}
let mut visited = vec![false; node_count];
let mut order = Vec::with_capacity(node_count);
for root in 0..node_count {
if visited[root] {
continue;
}
visited[root] = true;
let mut stack = vec![(root, 0usize)];
while let Some(&(node, next)) = stack.last() {
if let Some(&child) = forward[node].get(next) {
stack.last_mut().expect("frame just read").1 += 1;
if !visited[child] {
visited[child] = true;
stack.push((child, 0));
}
} else {
order.push(node);
stack.pop();
}
}
}
let mut component = vec![usize::MAX; node_count];
let mut component_size = Vec::new();
for &root in order.iter().rev() {
if component[root] != usize::MAX {
continue;
}
let id = component_size.len();
component_size.push(0usize);
component[root] = id;
let mut stack = vec![root];
while let Some(node) = stack.pop() {
component_size[id] += 1;
for &previous in &reverse[node] {
if component[previous] == usize::MAX {
component[previous] = id;
stack.push(previous);
}
}
}
}
for node in 0..node_count {
if component_size[component[node]] >= 2 {
cyclic[node] = true;
}
}
cyclic
}
fn recursive_declaration_names<'a>(
declarations: &BTreeMap<&'a str, &'a IDLType>,
max_work: usize,
budget: &mut crate::budget::Budget<'_>,
) -> Result<BTreeMap<&'a str, usize>, CompileError> {
let names: Vec<&str> = declarations.keys().copied().collect();
let index_of: BTreeMap<&str, usize> = names
.iter()
.enumerate()
.map(|(index, name)| (*name, index))
.collect();
let mut edges = Vec::new();
for (name, ty) in declarations {
let from = index_of[name];
let mut stack: Vec<&IDLType> = vec![ty];
while let Some(ty) = stack.pop() {
charge_type_preflight(
budget,
max_work,
1,
DiagnosticPhase::TypeCheck,
"type-depth preflight",
)?;
match ty {
IDLType::VarT(target) => {
if let Some(&to) = index_of.get(target.as_str()) {
edges.push((from, to));
}
}
IDLType::OptT(inner) | IDLType::VecT(inner) => stack.push(inner),
IDLType::RecordT(fields) | IDLType::VariantT(fields) => {
stack.extend(fields.iter().map(|field| &field.typ));
}
IDLType::FuncT(function) => {
stack.extend(function.args.iter().chain(&function.rets).map(|ty| &ty.typ));
}
IDLType::ServT(methods) => {
stack.extend(methods.iter().map(|method| &method.typ));
}
IDLType::ClassT(init, service) => {
stack.push(service);
stack.extend(init.iter().map(|ty| &ty.typ));
}
IDLType::PrimT(_) | IDLType::PrincipalT => {}
}
}
}
let cyclic = cyclic_nodes(names.len(), &edges);
Ok(names
.into_iter()
.zip(&cyclic)
.filter(|(_, &in_cycle)| in_cycle)
.enumerate()
.map(|(index, (name, _))| (name, index))
.collect())
}
pub(super) fn check_programs_type_depth<'a>(
programs: impl IntoIterator<Item = &'a IDLProg>,
budget: &mut crate::budget::Budget<'_>,
) -> Result<(), CompileError> {
let limits = budget.limits().clone();
let programs: Vec<_> = programs.into_iter().collect();
let mut declarations = BTreeMap::new();
for program in &programs {
for declaration in &program.decs {
if let Dec::TypD(binding) = declaration {
declarations
.entry(binding.id.as_str())
.or_insert(&binding.typ);
}
}
}
let max_work = limits.max_type_preflight_work;
let recursive = recursive_declaration_names(&declarations, max_work, budget)?;
let mut pending: Vec<_> = declarations
.values()
.copied()
.chain(
programs
.iter()
.filter_map(|program| program.actor.as_ref().map(|actor| &actor.typ)),
)
.map(|ty| (ty, 0usize, ActiveNames::default()))
.collect();
let mut visited = BTreeSet::new();
while let Some((ty, depth, active_names)) = pending.pop() {
charge_type_preflight(
budget,
max_work,
1usize.saturating_add(active_names.len()),
DiagnosticPhase::TypeCheck,
"type-depth preflight",
)?;
if depth > limits.max_type_depth {
return Err(CompileError::resource_limit(
"type_depth",
limits.max_type_depth,
depth,
format!(
"Candid type depth {depth} exceeds limit {}",
limits.max_type_depth
),
));
}
if !visited.insert((ty as *const IDLType as usize, depth, active_names.clone())) {
continue;
}
let next_depth = depth.saturating_add(1);
match ty {
IDLType::VarT(name) => {
if let Some(resolved) = declarations.get(name.as_str()).copied() {
let recursive_index = recursive.get(name.as_str()).copied();
if !recursive_index.is_some_and(|index| active_names.contains(&index)) {
let next_names = match recursive_index {
Some(index) => active_names.with(index),
None => active_names,
};
pending.push((resolved, depth, next_names));
}
}
}
IDLType::OptT(inner) | IDLType::VecT(inner) => {
pending.push((inner, next_depth, active_names));
}
IDLType::RecordT(fields) | IDLType::VariantT(fields) => {
for field in fields {
pending.push((&field.typ, next_depth, active_names.clone()));
}
}
IDLType::FuncT(function) => {
for ty in function.args.iter().chain(&function.rets) {
pending.push((&ty.typ, next_depth, active_names.clone()));
}
}
IDLType::ServT(methods) => {
for method in methods {
pending.push((&method.typ, next_depth, active_names.clone()));
}
}
IDLType::ClassT(init, service) => {
pending.push((service, next_depth, active_names.clone()));
for ty in init {
pending.push((&ty.typ, next_depth, active_names.clone()));
}
}
IDLType::PrimT(_) | IDLType::PrincipalT => {}
}
}
Ok(())
}
#[cfg(test)]
mod preflight_tests {
use super::*;
use crate::budget::Budget;
use crate::Limits;
fn preflight_work(source: &str) -> usize {
let limits = Limits::default();
let mut budget = Budget::from_limits(&limits);
let program: IDLProg = source.parse().unwrap();
check_programs_type_depth(std::iter::once(&program), &mut budget).unwrap();
budget.consumed("type_preflight_work")
}
fn shared_fanout(levels: usize) -> String {
let mut source = String::new();
for index in 1..=levels {
source.push_str(&format!(
"type T{index} = record {{ a: T{}; b: T{} }};\n",
index + 1,
index + 1
));
}
source.push_str(&format!("type T{} = nat;\n", levels + 1));
source.push_str("service : { go: (T1) -> () };");
source
}
fn longest_comment_run(text: &str) -> usize {
comment_run_preflight(text.as_bytes(), usize::MAX).unwrap()
}
#[test]
fn comment_runs_count_line_and_block_comments_between_real_tokens() {
assert_eq!(longest_comment_run(""), 0);
assert_eq!(longest_comment_run("type T = nat;"), 0);
assert_eq!(longest_comment_run("//\n//\n//\n"), 3);
assert_eq!(longest_comment_run("// a\n// b\ntype T = nat;"), 2);
assert_eq!(longest_comment_run("type T = nat;\n// a\n// b\n// c"), 3);
assert_eq!(longest_comment_run("// a\n/* b */\n// c\n/* d */ x"), 4);
assert_eq!(longest_comment_run("/* a *//* b *//* c */"), 3);
assert_eq!(longest_comment_run("// a\n// b\nx\n// c\ny"), 2);
assert_eq!(longest_comment_run("// a\nx // b\n// c\n// d\n"), 3);
assert_eq!(longest_comment_run("// a\n@\n// b"), 1);
assert_eq!(longest_comment_run("// a\n/ // b"), 1);
}
#[test]
fn comment_runs_ignore_whitespace_nesting_and_string_contents() {
assert_eq!(longest_comment_run(&"\n".repeat(10_000)), 0);
assert_eq!(longest_comment_run("// a\n\n\n \t\r\n// b\n"), 2);
assert_eq!(longest_comment_run("// a\r// b\r// c"), 1);
assert_eq!(longest_comment_run("// a\r\n// b\r\n"), 2);
assert_eq!(longest_comment_run("// a\r\n// b\r\n// c\r\n"), 3);
assert_eq!(longest_comment_run("/**/\r/**/\t/**/"), 3);
assert_eq!(longest_comment_run("// \u{e9}\n/* \u{1f600} */ // b"), 3);
assert_eq!(longest_comment_run("\"\u{e9} // a\" x"), 0);
assert_eq!(longest_comment_run("// a\n\u{e9}\n// b"), 1);
assert_eq!(longest_comment_run("/* /* /* */ */ */"), 1);
let deep = format!("{}{}", "/*".repeat(10_000), "*/".repeat(10_000));
assert_eq!(longest_comment_run(&deep), 1);
assert_eq!(longest_comment_run("/* // */ x"), 1);
assert_eq!(longest_comment_run("/* \" */ // b\n"), 2);
assert_eq!(longest_comment_run("// /* \n x"), 1);
assert_eq!(longest_comment_run(r#""// /* */""#), 0);
assert_eq!(longest_comment_run("// a\n\"//\"\n// b"), 1);
assert_eq!(longest_comment_run(r#""\" // a" x"#), 0);
assert_eq!(longest_comment_run(r#""\\" // a"#), 1);
}
#[test]
fn comment_runs_follow_the_upstream_block_comment_lexer() {
assert_eq!(longest_comment_run("/* //* */ */ // a"), 2);
assert_eq!(longest_comment_run("/* **/ // a"), 2);
assert_eq!(longest_comment_run("/*/ x */ // a"), 2);
assert_eq!(longest_comment_run("/**/ /***/ // a"), 3);
assert_eq!(longest_comment_run("// a\n/* never closed // b"), 2);
assert_eq!(longest_comment_run("x \"never closed // a"), 0);
}
#[test]
fn comment_run_refusal_reports_the_crossing_count() {
let run = "//\n".repeat(10);
assert_eq!(comment_run_preflight(run.as_bytes(), 10), Ok(10));
assert_eq!(comment_run_preflight(run.as_bytes(), 9), Err(10));
assert_eq!(comment_run_preflight(run.as_bytes(), 3), Err(4));
assert_eq!(comment_run_preflight(b"type T = nat;", 0), Ok(0));
assert_eq!(comment_run_preflight(b"// a", 0), Err(1));
}
#[test]
fn comment_runs_agree_with_the_upstream_tokenizer() {
const ALPHABET: &[u8] = b"/*\"\\\n\r\t a";
fn gap_comments(gap: &str) -> usize {
let (mut rest, mut longest) = (gap, 0);
loop {
rest = rest.trim_start_matches([' ', '\t', '\r', '\n']);
if rest.is_empty() {
return longest;
}
longest += 1;
if let Some(line) = rest.strip_prefix("//") {
rest = line.find('\n').map_or("", |end| &line[end..]);
} else {
let mut body = rest.strip_prefix("/*").expect("a gap holds only comments");
let mut depth = 1;
while depth > 0 {
let open = body.find("/*");
let close = body.find("*/").expect("a valid gap closes its comments");
if open.is_some_and(|open| open < close) {
depth += 1;
body = &body[open.unwrap() + 2..];
} else {
depth -= 1;
body = &body[close + 2..];
}
}
rest = body;
}
}
}
let mut compared = 0usize;
let mut text = Vec::new();
for length in 0..=7u32 {
for mut code in 0..ALPHABET.len().pow(length) {
text.clear();
for _ in 0..length {
text.push(ALPHABET[code % ALPHABET.len()]);
code /= ALPHABET.len();
}
let source = std::str::from_utf8(&text).unwrap();
let Ok(tokens) = Tokenizer::new(source).collect::<Result<Vec<_>, _>>() else {
continue;
};
let mut expected = 0;
let mut gap_start = 0;
for (start, _, end) in &tokens {
expected = expected.max(gap_comments(&source[gap_start..*start]));
gap_start = *end;
}
expected = expected.max(gap_comments(&source[gap_start..]));
assert_eq!(longest_comment_run(source), expected, "{source:?}");
compared += 1;
}
}
assert_eq!(compared, 392_192);
}
#[test]
fn cyclic_nodes_marks_self_loops_and_cycle_members_only() {
assert_eq!(cyclic_nodes(3, &[(0, 1), (1, 2)]), [false, false, false]);
assert_eq!(cyclic_nodes(2, &[(0, 0), (0, 1)]), [true, false]);
assert_eq!(
cyclic_nodes(3, &[(0, 1), (1, 0), (1, 2)]),
[true, true, false]
);
assert_eq!(
cyclic_nodes(4, &[(0, 1), (0, 2), (1, 3), (2, 3), (3, 0)]),
[true, true, true, true]
);
assert_eq!(cyclic_nodes(2, &[(0, 1), (0, 1)]), [false, false]);
}
#[test]
fn minimal_program_charges_exactly_six_units() {
assert_eq!(
preflight_work("type T = nat; service : { f: (T) -> () };"),
6
);
}
#[test]
fn recursive_names_cost_one_extra_unit_per_state_inside_their_expansion() {
assert_eq!(
preflight_work("type L = opt L; type M = record { a: L; b: L }; service : {};"),
19
);
}
#[test]
fn shared_subtrees_deduplicate_instead_of_doubling_per_level() {
assert_eq!(preflight_work(&shared_fanout(6)), 138);
assert_eq!(preflight_work(&shared_fanout(12)), 414);
assert_eq!(preflight_work(&shared_fanout(24)), 1_398);
}
#[test]
fn alias_chains_cost_a_constant_per_declaration() {
let alias_chain = |length: usize| {
let mut source = String::new();
for index in 1..length {
source.push_str(&format!("type A{index} = A{};\n", index + 1));
}
source.push_str(&format!("type A{length} = nat;\n"));
source.push_str("service : { f: (A1) -> () };");
source
};
assert_eq!(preflight_work(&alias_chain(100)), 402);
assert_eq!(preflight_work(&alias_chain(500)), 2_002);
assert_eq!(preflight_work(&alias_chain(1_000)), 4_002);
assert_eq!(preflight_work(&alias_chain(2_000)), 8_002);
}
}