Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ jobs:
- text-similarity
- command-arguments
- config-toml
- source-cpp
- terminal-style
- terminal-input
- event-stream
Expand Down
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ secure-random = ["dep:getrandom"]
text-similarity = ["dep:strsim"]
command-arguments = ["dep:shell-words"]
config-toml = ["dep:toml"]
source-cpp = ["dep:tree-sitter", "dep:tree-sitter-cpp"]
terminal-style = []
# Native terminal capture and key decoding without PTY process spawning.
terminal-input = []
Expand Down Expand Up @@ -226,6 +227,8 @@ getrandom = { version = "=0.4.3", optional = true }
strsim = { version = "=0.11.1", optional = true }
toml = { version = "=0.8.23", optional = true }
shell-words = { version = "=1.1.1", optional = true }
tree-sitter = { version = "=0.26.11", optional = true }
tree-sitter-cpp = { version = "=0.23.4", optional = true }
globset = { version = "=0.4.18", optional = true }
interprocess = { version = "=2.4.3", optional = true }
jwalk = { version = "=0.8.1", optional = true }
Expand Down Expand Up @@ -387,6 +390,7 @@ toml = "=0.8.23"
features = [
"full",
"config-toml",
"source-cpp",
"daemon-identity",
"daemon-frame-v1",
"daemon-registration",
Expand Down
2 changes: 2 additions & 0 deletions ci/check_compilation_boundary_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
("pty", "portable-pty"),
("text-similarity", "strsim"),
("command-arguments", "shell-words"),
("source-cpp", "tree-sitter"),
("source-cpp", "tree-sitter-cpp"),
("wasm-sketch-host", "wasmtime"),
("ipc", "interprocess"),
("tokio-console", "console-subscriber"),
Expand Down
32 changes: 32 additions & 0 deletions docs/source-cpp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# C++ function analysis

The optional `source-cpp` feature privately owns an exact-pinned C++ grammar and
parser. `source::analyze_cpp` returns owned `FunctionDefinition` records in source
order, with byte ranges and namespace/aggregate/explicit-linkage context flags.
It does not expose parser nodes, trees, grammar symbols, or backend traits.

The implementation retains provenance from FastLED/fbuild's source scanner at
`1e75ccf5a4ca922b4d922a6da286b965fac8832d`, via the FastLED WASM adapter. It uses
syntax nodes to remove function parameter defaults, rather than splitting on
commas or guessing expression nesting. Headers retain their source formatting,
including newlines terminating line comments. Do not flatten or trim those
newlines before appending a declaration terminator.
They are not guaranteed to be standalone declarations: this is syntax analysis,
not a C++ compiler, preprocessor, or name-resolution service.

Applications own record selection, deduplication, Arduino `setup`/`loop` policy,
tab order, generated includes, source maps, and editor publication. Definitions
inside another function body are not inventoried. Invalid or incomplete syntax
returns an error without partial records, allowing an editor to keep its last
good product output.

The source limit is 8 MiB, checked before parsing. The parser observes a
two-second cooperative deadline; this is not process containment or a hard CPU
deadline. Subsequent iterative traversal is limited to 131,072 node visits,
depth 256, and 16,384 output records. Signature traversal shares the visit budget.
These are traversal/output bounds, not independent parser allocation quotas.
Diagnostics do not contain source contents. No filesystem, runtime, or ambient
process state is created by analysis.

Issue #209 tracks contract expansion, platform verification, and FastLED
adoption. The initial implementation is not yet release-accepted.
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub mod config;
/// an incremental hasher and key-derivation domain separation.
pub mod hash;

/// Bounded source analysis; callers own language integration and product policy.
#[cfg(feature = "source-cpp")]
pub mod source;

/// Bounded, fallible operating-system entropy without token-format policy.
#[cfg(feature = "secure-random")]
pub mod random;
Expand Down
195 changes: 195 additions & 0 deletions src/source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//! C++ syntax mechanics adapted from FastLED/fbuild's source scanner at
//! 1e75ccf5a4ca922b4d922a6da286b965fac8832d via fastled-wasm. No Arduino policy.

use std::ops::{ControlFlow, Range};
use std::time::{Duration, Instant};
use tree_sitter::{Node, ParseOptions, Parser};

pub const MAX_SOURCE_BYTES: usize = 8 * 1024 * 1024;
pub const MAX_FUNCTIONS: usize = 16_384;
pub const MAX_VISITED_NODES: usize = 131_072;
pub const MAX_DEPTH: usize = 256;

/// Enclosing syntax contexts, not compiler-resolved semantic scopes.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct FunctionContext {
pub namespace: bool,
pub aggregate: bool,
pub explicit_linkage: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FunctionDefinition {
/// Definition header without its body or function parameter defaults.
/// Formatting and comments are retained. This is not a semantic C++ compiler
/// and does not promise that every header can become a standalone declaration.
pub signature: String,
pub source_range: Range<usize>,
pub context: FunctionContext,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum AnalysisError {
#[error("C++ source exceeds the byte limit")]
InputTooLarge,
#[error("C++ syntax is incomplete or invalid")]
InvalidSyntax,
#[error("C++ parsing exceeded its cooperative deadline")]
TimedOut,
#[error("C++ analysis exceeded its traversal or output limit")]
LimitExceeded,
#[error("C++ definition form is unsupported")]
UnsupportedDefinition,
#[error("C++ parser initialization failed")]
ParserUnavailable,
}

/// Analyze definitions in source order, without deduplication or name filtering.
/// Definitions nested inside a function body are not inventoried.
///
/// The byte limit is checked before parsing. A two-second cooperative parser
/// deadline is not a hard CPU deadline. Traversal depth/node and record limits
/// apply afterward; they are not independent parser-allocation quotas. Failure
/// returns no partial records. Source is never included in errors.
pub fn analyze_cpp(source: &str) -> Result<Vec<FunctionDefinition>, AnalysisError> {
if source.len() > MAX_SOURCE_BYTES {
return Err(AnalysisError::InputTooLarge);
}
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.map_err(|_| AnalysisError::ParserUnavailable)?;
let started = Instant::now();
let mut progress = |_: &tree_sitter::ParseState| {
if started.elapsed() >= Duration::from_secs(2) {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
};
let bytes = source.as_bytes();
let tree = parser
.parse_with_options(
&mut |offset, _| bytes.get(offset..).unwrap_or_default(),
None,
Some(ParseOptions::new().progress_callback(&mut progress)),
)
.ok_or(AnalysisError::TimedOut)?;
if tree.root_node().has_error() {
return Err(AnalysisError::InvalidSyntax);
}
let mut remaining = MAX_VISITED_NODES;
let mut pending = vec![(tree.root_node(), 0, FunctionContext::default())];
let mut output = Vec::new();
while let Some((node, depth, mut context)) = pending.pop() {
charge(&mut remaining, depth)?;
match node.kind() {
"namespace_definition" => context.namespace = true,
"class_specifier" | "struct_specifier" | "union_specifier" => context.aggregate = true,
"linkage_specification" => context.explicit_linkage = true,
"function_definition" => {
if output.len() == MAX_FUNCTIONS {
return Err(AnalysisError::LimitExceeded);
}
output.push(definition(node, source, context, depth, &mut remaining)?);
continue;
}
_ => {}
}
if pending.len().saturating_add(node.child_count()) > remaining {
return Err(AnalysisError::LimitExceeded);
}
for index in (0..node.child_count()).rev() {
let index = u32::try_from(index).map_err(|_| AnalysisError::LimitExceeded)?;
if let Some(child) = node.child(index) {
pending.push((child, depth + 1, context));
}
}
}
Ok(output)
}

fn charge(remaining: &mut usize, depth: usize) -> Result<(), AnalysisError> {
if depth > MAX_DEPTH {
return Err(AnalysisError::LimitExceeded);
}
*remaining = remaining
.checked_sub(1)
.ok_or(AnalysisError::LimitExceeded)?;
Ok(())
}

fn definition(
node: Node<'_>,
source: &str,
context: FunctionContext,
root_depth: usize,
remaining: &mut usize,
) -> Result<FunctionDefinition, AnalysisError> {
let header = node
.parent()
.filter(|parent| parent.kind() == "template_declaration")
.unwrap_or(node);
let body = node
.child_by_field_name("body")
.ok_or(AnalysisError::UnsupportedDefinition)?;
let range = header.start_byte()..body.start_byte();
let mut removals = Vec::new();
let mut pending = vec![(node, root_depth)];
while let Some((part, depth)) = pending.pop() {
charge(remaining, depth)?;
if part.start_byte() >= range.end {
continue;
}
if part.kind() == "optional_parameter_declaration" {
let mut cursor = part.walk();
let equals = part
.children(&mut cursor)
.find(|child| child.kind() == "=")
.ok_or(AnalysisError::UnsupportedDefinition)?;
let mut start = equals.start_byte();
// A newline can terminate a preceding // comment. Removing it
// would comment out the following comma or closing parenthesis.
while start > part.start_byte() && matches!(source.as_bytes()[start - 1], b' ' | b'\t')
{
start -= 1;
}
removals.push(start..part.end_byte());
continue;
}
if pending.len().saturating_add(part.child_count()) > *remaining {
return Err(AnalysisError::LimitExceeded);
}
for index in (0..part.child_count()).rev() {
let index = u32::try_from(index).map_err(|_| AnalysisError::LimitExceeded)?;
if let Some(child) = part.child(index) {
pending.push((child, depth + 1));
}
}
}
let original = source
.get(range.clone())
.ok_or(AnalysisError::UnsupportedDefinition)?;
// Copy retained spans once. Repeated in-place deletion would make a wide
// parameter list quadratic even though its source and node count are bounded.
let mut signature = String::with_capacity(original.len());
let mut retained_start = range.start;
for remove in removals {
if remove.start < retained_start || remove.end > range.end {
return Err(AnalysisError::UnsupportedDefinition);
}
signature.push_str(&source[retained_start..remove.start]);
retained_start = remove.end;
}
signature.push_str(&source[retained_start..range.end]);
Ok(FunctionDefinition {
// Likewise keep a trailing newline before the removed function body:
// the caller may append a semicolon after a line-commented header.
signature: signature
.trim_start()
.trim_end_matches([' ', '\t'])
.to_owned(),
source_range: range,
context,
})
}
Loading
Loading