Add private mixed bundle syntax witness over shared parser and traversal
This commit is contained in:
+178
-28
@@ -1,8 +1,8 @@
|
||||
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2/V3/V4 migration.
|
||||
mod raw_tokens;
|
||||
#[path = "language/semantic_source.rs"]
|
||||
mod semantic_source;
|
||||
mod source3_bridge;
|
||||
mod raw_tokens;
|
||||
// Staged syntax foundation only. Public source8 admission waits for complete
|
||||
// declaration lowering, source updates and the root-owned resource gate.
|
||||
#[cfg(test)]
|
||||
@@ -480,6 +480,8 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result<ParsedDocument, V
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits,
|
||||
#[cfg(test)]
|
||||
mixed: None,
|
||||
}
|
||||
.document()
|
||||
.map_err(|e| vec![diagnostic(source, e)])
|
||||
@@ -914,6 +916,8 @@ pub(crate) fn parse_unit(
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits,
|
||||
#[cfg(test)]
|
||||
mixed: None,
|
||||
}
|
||||
.unit(entry, true)
|
||||
.map_err(|e| Box::new(diagnostic(source, e)))
|
||||
@@ -1005,31 +1009,52 @@ fn edit_positions(
|
||||
unit: SourceUnit,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
let tokens = strict_tokens(source).map_err(|e| Box::new(diagnostic(source, e)))?;
|
||||
let mut ends: Vec<_> = tokens.iter().map(|token| token.span.end).collect();
|
||||
edit_positions_from_tokens(source, &unit, |at| tokens[at], tokens.len(), None)
|
||||
}
|
||||
|
||||
fn edit_positions_from_tokens(
|
||||
source: &str,
|
||||
unit: &SourceUnit,
|
||||
token: impl Fn(usize) -> Lexeme,
|
||||
count: usize,
|
||||
value_ends: Option<&BTreeMap<usize, usize>>,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
let mut ends: Vec<_> = (0..count).map(|at| token(at).span.end).collect();
|
||||
let mut stack = Vec::new();
|
||||
for (index, token) in tokens.iter().enumerate() {
|
||||
for index in 0..count {
|
||||
let token = token(index);
|
||||
match token.text(source) {
|
||||
"{" | "[" => stack.push(index),
|
||||
"}" | "]" => ends[stack.pop().expect("parser checked delimiters")] = token.span.end,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
struct Positions<'a> {
|
||||
tokens: &'a [Lexeme],
|
||||
struct Positions<'a, F> {
|
||||
token_at: F,
|
||||
ends: Vec<usize>,
|
||||
value_ends: Option<&'a BTreeMap<usize, usize>>,
|
||||
}
|
||||
impl Positions<'_> {
|
||||
impl<F: Fn(usize) -> Lexeme> Positions<'_, F> {
|
||||
fn token(&self, start: usize) -> usize {
|
||||
self.tokens
|
||||
.partition_point(|token| token.span.start < start)
|
||||
let mut low = 0;
|
||||
let mut high = self.ends.len();
|
||||
while low < high {
|
||||
let middle = low + (high - low) / 2;
|
||||
if (self.token_at)(middle).span.start < start {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
low
|
||||
}
|
||||
fn fields(&self, fields: &BTreeMap<String, Ast>) -> BTreeMap<String, EditField> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|(key, ast)| {
|
||||
let value = self.value(ast);
|
||||
let start = self.tokens[self.token(value.span.start) - 1].span.start;
|
||||
let end = self.tokens[self.token(value.span.end)].span.end;
|
||||
let start = (self.token_at)(self.token(value.span.start) - 1).span.start;
|
||||
let end = (self.token_at)(self.token(value.span.end)).span.end;
|
||||
(
|
||||
key.clone(),
|
||||
EditField {
|
||||
@@ -1044,10 +1069,15 @@ fn edit_positions(
|
||||
let index = self.token(ast.offset());
|
||||
let span = Span {
|
||||
start: ast.offset(),
|
||||
end: match ast {
|
||||
Ast::Reference(..) | Ast::HistoricalUuid(..) => self.tokens[index + 1].span.end,
|
||||
_ => self.ends[index],
|
||||
},
|
||||
end: self
|
||||
.value_ends
|
||||
.and_then(|ends| ends.get(&ast.offset()).copied())
|
||||
.unwrap_or_else(|| match ast {
|
||||
Ast::Reference(..) | Ast::HistoricalUuid(..) => {
|
||||
(self.token_at)(index + 1).span.end
|
||||
}
|
||||
_ => self.ends[index],
|
||||
}),
|
||||
};
|
||||
let kind = match ast {
|
||||
Ast::Object(fields, _) => EditKind::Record(self.fields(fields)),
|
||||
@@ -1062,11 +1092,12 @@ fn edit_positions(
|
||||
}
|
||||
}
|
||||
let positions = Positions {
|
||||
tokens: &tokens,
|
||||
token_at: &token,
|
||||
ends,
|
||||
value_ends,
|
||||
};
|
||||
unit.declarations
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|declaration| {
|
||||
if declaration.id_insertion.is_some() {
|
||||
return Err(Box::new(diagnostic(
|
||||
@@ -1086,8 +1117,8 @@ fn edit_positions(
|
||||
let start = positions.token(declaration.span.start);
|
||||
let header = roles
|
||||
.iter()
|
||||
.zip(&tokens[start..])
|
||||
.map(|(role, token)| (*role, token.span))
|
||||
.zip(start..count)
|
||||
.map(|(role, at)| (*role, token(at).span))
|
||||
.collect();
|
||||
Ok((
|
||||
declaration.id,
|
||||
@@ -1114,10 +1145,35 @@ struct Parser<'a> {
|
||||
cursor: usize,
|
||||
value_depth: usize,
|
||||
limits: ParseLimits,
|
||||
#[cfg(test)]
|
||||
mixed: Option<analysis_source::mixed::State<'a>>,
|
||||
}
|
||||
impl Parser<'_> {
|
||||
fn token(&self, index: usize) -> Option<Lexeme> {
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &self.mixed {
|
||||
return mixed.token(index);
|
||||
}
|
||||
self.tokens.get(index).copied()
|
||||
}
|
||||
fn current(&self) -> Lexeme {
|
||||
self.tokens[self.cursor.min(self.tokens.len() - 1)]
|
||||
self.token(self.cursor).unwrap_or(Lexeme {
|
||||
class: TokenClass::Punctuation,
|
||||
span: Span {
|
||||
start: self.source.len(),
|
||||
end: self.source.len(),
|
||||
},
|
||||
})
|
||||
}
|
||||
fn syntax_entry(&mut self) -> Result<(), Error> {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let span = self.current().span;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.entry(span)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn text(&self) -> &str {
|
||||
self.current().text(self.source)
|
||||
@@ -1154,6 +1210,10 @@ impl Parser<'_> {
|
||||
}
|
||||
fn optional_id(&mut self) -> Result<Id, Error> {
|
||||
if self.text() != "id" {
|
||||
#[cfg(test)]
|
||||
if self.mixed.is_some() {
|
||||
return Err(self.error("Private mixed syntax requires explicit IDs."));
|
||||
}
|
||||
return Ok(Id::new_v4());
|
||||
}
|
||||
self.cursor += 1;
|
||||
@@ -1194,6 +1254,7 @@ impl Parser<'_> {
|
||||
Ok(fields)
|
||||
}
|
||||
fn field(&mut self) -> Result<(String, Ast, Span), Error> {
|
||||
self.syntax_entry()?;
|
||||
let key = self.text().to_owned();
|
||||
if !matches!(
|
||||
self.current().class,
|
||||
@@ -1204,15 +1265,19 @@ impl Parser<'_> {
|
||||
self.cursor += 1;
|
||||
let start = self.current().span.start;
|
||||
let value = self.value()?;
|
||||
let end = self.tokens[self.cursor - 1].span.end;
|
||||
let end = self
|
||||
.token(self.cursor - 1)
|
||||
.expect("consumed token")
|
||||
.span
|
||||
.end;
|
||||
self.take(";")?;
|
||||
Ok((key, value, Span { start, end }))
|
||||
}
|
||||
|
||||
fn nested_header_kind(&self) -> Option<DeclKind> {
|
||||
let kind = declaration_kind(self.text())?;
|
||||
let alias = self.tokens.get(self.cursor + 1)?;
|
||||
let following = self.tokens.get(self.cursor + 2)?;
|
||||
let alias = self.token(self.cursor + 1)?;
|
||||
let following = self.token(self.cursor + 2)?;
|
||||
// A node's ordinary `diagram @view;` field is not a declaration.
|
||||
if !crate::valid_alias(alias.text(self.source)) {
|
||||
return None;
|
||||
@@ -1227,14 +1292,37 @@ impl Parser<'_> {
|
||||
if self.value_depth >= 64 {
|
||||
return Err(self.error("Typed values may not nest more than 64 levels."));
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
self.value_depth += 1;
|
||||
let result = self.value_inner();
|
||||
self.value_depth -= 1;
|
||||
#[cfg(test)]
|
||||
if let Ok(ast) = &result {
|
||||
let end = self
|
||||
.token(self.cursor - 1)
|
||||
.expect("consumed value")
|
||||
.span
|
||||
.end;
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.value_ends.insert(ast.offset(), end);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
fn value_inner(&mut self) -> Result<Ast, Error> {
|
||||
let token = self.current();
|
||||
let offset = token.span.start;
|
||||
#[cfg(test)]
|
||||
if self.mixed.is_some()
|
||||
&& (token.class == TokenClass::Number || matches!(self.text(), "-" | "+"))
|
||||
{
|
||||
let value = self
|
||||
.mixed
|
||||
.as_ref()
|
||||
.expect("mixed view")
|
||||
.ordinary_number(&mut self.cursor)?;
|
||||
return Ok(Ast::Literal(value, offset));
|
||||
}
|
||||
match self.text() {
|
||||
"uuid" => {
|
||||
self.cursor += 1;
|
||||
@@ -1332,16 +1420,31 @@ impl Parser<'_> {
|
||||
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
|
||||
let mut header = if entry {
|
||||
self.take("arcadia")?;
|
||||
if !matches!(self.text(), "3" | "4" | "5" | "6" | "7") {
|
||||
return Err(
|
||||
self.error("Unsupported schema version: expected arcadia 3, 4, 5, 6 or 7.")
|
||||
);
|
||||
#[cfg(test)]
|
||||
let mixed_mode = self.mixed.is_some();
|
||||
#[cfg(not(test))]
|
||||
let mixed_mode = false;
|
||||
if if mixed_mode {
|
||||
self.text() != "8"
|
||||
} else {
|
||||
!matches!(self.text(), "3" | "4" | "5" | "6" | "7")
|
||||
} {
|
||||
return Err(self.error(if mixed_mode {
|
||||
"Private mixed syntax requires arcadia 8."
|
||||
} else {
|
||||
"Unsupported schema version: expected arcadia 3, 4, 5, 6 or 7."
|
||||
}));
|
||||
}
|
||||
let schema_version = self.text().parse().expect("checked source version");
|
||||
self.cursor += 1;
|
||||
self.take(";")?;
|
||||
let start = self.current().span.start;
|
||||
self.take("project")?;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.record(false, Span { start, end: start })?;
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
let alias = self.identifier()?;
|
||||
let name = self.quoted()?;
|
||||
let id_insertion = (self.text() != "id").then_some(self.current().span.start);
|
||||
@@ -1382,6 +1485,17 @@ impl Parser<'_> {
|
||||
let mut open: Vec<usize> = Vec::new();
|
||||
loop {
|
||||
let offset = self.current().span.start;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &self.mixed {
|
||||
if mixed.typed_header(self.cursor, open.is_empty()) {
|
||||
let owner = open.last().map(|&at| declarations[at].id);
|
||||
self.mixed
|
||||
.as_mut()
|
||||
.expect("mixed view")
|
||||
.parse_typed(&mut self.cursor, owner)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(&owner) = open.last() {
|
||||
if self.text() == "}" {
|
||||
declarations[owner].body.end = offset;
|
||||
@@ -1425,6 +1539,14 @@ impl Parser<'_> {
|
||||
if !includes_allowed {
|
||||
return Err(self.error("An include requires a source bundle; single-string parsing never reads files."));
|
||||
}
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.include(Span {
|
||||
start: offset,
|
||||
end: offset,
|
||||
})?;
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
self.cursor += 1;
|
||||
let path = self.quoted()?;
|
||||
self.take(";")?;
|
||||
@@ -1432,7 +1554,11 @@ impl Parser<'_> {
|
||||
path,
|
||||
Span {
|
||||
start: offset,
|
||||
end: self.tokens[self.cursor - 1].span.end,
|
||||
end: self
|
||||
.token(self.cursor - 1)
|
||||
.expect("consumed token")
|
||||
.span
|
||||
.end,
|
||||
},
|
||||
));
|
||||
continue;
|
||||
@@ -1444,20 +1570,40 @@ impl Parser<'_> {
|
||||
if entry_header.description.is_some() {
|
||||
return Err(self.error("Duplicate project description."));
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
self.cursor += 1;
|
||||
let description = self.quoted()?;
|
||||
self.take(";")?;
|
||||
entry_header.project.description = description;
|
||||
entry_header.description = Some(Span {
|
||||
start: offset,
|
||||
end: self.tokens[self.cursor - 1].span.end,
|
||||
end: self
|
||||
.token(self.cursor - 1)
|
||||
.expect("consumed token")
|
||||
.span
|
||||
.end,
|
||||
});
|
||||
header = Some(entry_header);
|
||||
continue;
|
||||
}
|
||||
#[cfg(test)]
|
||||
if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) {
|
||||
return Err(self.error("Use the short source8 analysis declaration word."));
|
||||
}
|
||||
self.syntax_entry()?;
|
||||
let word = self.text().to_owned();
|
||||
let kind = declaration_kind(&word)
|
||||
.ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?;
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.record(
|
||||
matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)),
|
||||
Span {
|
||||
start: offset,
|
||||
end: offset,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
graph_count += usize::from(matches!(
|
||||
kind,
|
||||
DeclKind::Element(..) | DeclKind::Relation(..)
|
||||
@@ -1485,6 +1631,10 @@ impl Parser<'_> {
|
||||
let body_start = self.current().span.end;
|
||||
self.take("{")?;
|
||||
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
|
||||
#[cfg(test)]
|
||||
if let Some(mixed) = &mut self.mixed {
|
||||
mixed.lexical_owners.push(open.last().copied());
|
||||
}
|
||||
declarations.push(Declaration {
|
||||
kind,
|
||||
alias,
|
||||
|
||||
Reference in New Issue
Block a user