Skip to content

Commit eeb9268

Browse files
authored
vhdl_syntax: Enable parsing under different VHDL standards and add standards API (#444)
* Introduce VHDL standard API * Parse keywords selectively based on the tokenizer standard * Add VHDL standard to the Parser and add toy VHDL2019 example
1 parent 40d5ea1 commit eeb9268

10 files changed

Lines changed: 527 additions & 141 deletions

vhdl_syntax/examples/source_refactoring.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
//
1515
// Copyright (c) 2025, Lukas Scheller lukasscheller@icloud.com
1616
use vhdl_syntax::parser;
17-
use vhdl_syntax::parser::Parser;
1817
use vhdl_syntax::syntax::node::SyntaxElement;
1918
use vhdl_syntax::syntax::rewrite::RewriteAction;
2019
use vhdl_syntax::syntax::AstNode;
@@ -38,22 +37,22 @@ end foobar;
3837
"Did not expect diagnostics for correct VHDL"
3938
);
4039

41-
// The target
42-
// NOTE: Usage of the parser API will be significantly changed or removed in favor of a better alternative in a future version.
43-
let mut parser = Parser::new(
40+
// The target: parse a full design file and take the entity declaration node.
41+
let (replacement_file, diagnostics) = parser::parse(
4442
"\
4543
entity no_longer_foo is
4644
end no_longer_foo;
4745
48-
"
49-
.into(),
46+
",
5047
);
51-
parser.entity_declaration();
52-
let (replacement_entity, diagnostics) = parser.into_root();
5348
assert!(
5449
diagnostics.is_empty(),
5550
"Did not expect diagnostics for correct VHDL"
5651
);
52+
let replacement_entity = replacement_file
53+
.design_units()
54+
.next()
55+
.expect("expected entity declaration in replacement file");
5756

5857
let new_file = file.raw().rewrite(|node| match node {
5958
SyntaxElement::Node(node) => match EntityDeclarationSyntax::cast(node.clone()) {
@@ -64,7 +63,7 @@ end no_longer_foo;
6463
.and_then(|preamble| preamble.name_token())
6564
.is_some_and(|tok| tok.text() == "foo") =>
6665
{
67-
RewriteAction::Change(SyntaxElement::Node(replacement_entity.clone()))
66+
RewriteAction::Change(SyntaxElement::Node(replacement_entity.raw()))
6867
}
6968
// If the syntax node is not an entity, or the name of the entity is not 'foo', leave the node as-is
7069
_ => RewriteAction::Leave,

vhdl_syntax/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod latin_1;
99
pub mod parser;
1010
#[cfg(feature = "serde")]
1111
pub mod serde;
12+
pub mod standard;
1213
pub mod syntax;
1314
mod token_interning;
1415
pub mod tokens;

vhdl_syntax/src/parser/mod.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
// You can obtain one at http://mozilla.org/MPL/2.0/.
55
//
66
// Copyright (c) 2024, Lukas Scheller lukasscheller@icloud.com
7+
use crate::standard::VHDLStandard;
78
use crate::syntax::node::SyntaxNode;
89
use crate::syntax::{DesignFileSyntax, NodeKind};
910
use crate::tokens::TokenStream;
11+
use crate::tokens::Tokenizer;
1012

1113
mod builder;
1214
pub mod diagnostics;
@@ -26,29 +28,50 @@ pub struct Parser {
2628
builder: builder::NodeBuilder,
2729
diagnostics: Vec<diagnostics::ParserDiagnostic>,
2830
unexpected_eof: bool,
31+
standard: VHDLStandard,
2932
}
3033

3134
impl Parser {
32-
pub fn new(token_stream: TokenStream) -> Parser {
35+
pub(crate) fn new(token_stream: TokenStream, standard: VHDLStandard) -> Parser {
3336
Parser {
3437
token_stream,
3538
builder: builder::NodeBuilder::new(),
3639
diagnostics: Vec::default(),
3740
unexpected_eof: false,
41+
standard,
3842
}
3943
}
4044

45+
pub fn standard(&self) -> VHDLStandard {
46+
self.standard
47+
}
48+
4149
pub fn into_root(self) -> (SyntaxNode, Vec<diagnostics::ParserDiagnostic>) {
4250
let (green, diagnostics) = self.end();
4351
(SyntaxNode::new_root(green), diagnostics)
4452
}
4553
}
4654

47-
/// Parse and return a VHDL file.
55+
/// Parse and return a VHDL file using the default VHDL standard.
56+
///
57+
/// Use [`parse_with_standard`] to use a non-default VHDL standard.
4858
pub fn parse(
4959
token_stream: impl Into<TokenStream>,
5060
) -> (DesignFileSyntax, Vec<diagnostics::ParserDiagnostic>) {
51-
let mut parser: Parser = Parser::new(token_stream.into());
61+
let mut parser = Parser::new(token_stream.into(), VHDLStandard::default());
62+
parser.design_file();
63+
let (syntax_node, diagnostics) = parser.into_root();
64+
debug_assert!(syntax_node.kind() == NodeKind::DesignFile);
65+
(DesignFileSyntax(syntax_node), diagnostics)
66+
}
67+
68+
/// Parse and return a VHDL file, tokenizing and parsing under the given `standard`.
69+
pub fn parse_with_standard(
70+
standard: VHDLStandard,
71+
input: impl IntoIterator<Item = u8>,
72+
) -> (DesignFileSyntax, Vec<diagnostics::ParserDiagnostic>) {
73+
let token_stream: TokenStream = Tokenizer::with_standard(standard, input.into_iter()).collect();
74+
let mut parser = Parser::new(token_stream, standard);
5275
parser.design_file();
5376
let (syntax_node, diagnostics) = parser.into_root();
5477
debug_assert!(syntax_node.kind() == NodeKind::DesignFile);
@@ -60,7 +83,20 @@ pub(crate) fn parse_syntax(
6083
token_stream: impl Into<TokenStream>,
6184
parser_fn: impl FnOnce(&mut Parser),
6285
) -> (SyntaxNode, Vec<diagnostics::ParserDiagnostic>) {
63-
let mut parser: Parser = Parser::new(token_stream.into());
86+
let mut parser = Parser::new(token_stream.into(), VHDLStandard::default());
87+
parser_fn(&mut parser);
88+
let (green, diagnostics) = parser.end();
89+
(SyntaxNode::new_root(green), diagnostics)
90+
}
91+
92+
#[cfg(test)]
93+
pub(crate) fn parse_syntax_with_standard(
94+
standard: VHDLStandard,
95+
input: impl IntoIterator<Item = u8>,
96+
parser_fn: impl FnOnce(&mut Parser),
97+
) -> (SyntaxNode, Vec<diagnostics::ParserDiagnostic>) {
98+
let token_stream: TokenStream = Tokenizer::with_standard(standard, input.into_iter()).collect();
99+
let mut parser = Parser::new(token_stream, standard);
64100
parser_fn(&mut parser);
65101
let (green, diagnostics) = parser.end();
66102
(SyntaxNode::new_root(green), diagnostics)

vhdl_syntax/src/parser/productions/component_declaration.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Copyright (c) 2025, Lukas Scheller lukasscheller@icloud.com
66

77
use crate::parser::Parser;
8+
use crate::standard::VHDLStandard;
89
use crate::syntax::node_kind::NodeKind::*;
910
use crate::tokens::token_kind::Keyword as Kw;
1011
use crate::tokens::TokenKind::*;
@@ -31,7 +32,12 @@ impl Parser {
3132

3233
pub fn component_declaration_epilogue(&mut self) {
3334
self.start_node(ComponentDeclarationEpilogue);
34-
self.expect_tokens([Keyword(Kw::End), Keyword(Kw::Component)]);
35+
self.expect_token(Keyword(Kw::End));
36+
if self.standard().is_at_least(VHDLStandard::VHDL2019) {
37+
self.opt_token(Keyword(Kw::Component));
38+
} else {
39+
self.expect_token(Keyword(Kw::Component));
40+
}
3541
self.opt_identifier();
3642
self.expect_token(SemiColon);
3743
self.end_node();
@@ -40,8 +46,9 @@ impl Parser {
4046

4147
#[cfg(test)]
4248
mod tests {
43-
use crate::parser::test_utils::to_test_text;
44-
use crate::parser::Parser;
49+
use crate::parser::test_utils::{to_test_text, to_test_text_with_standard};
50+
use crate::parser::{parse_syntax_with_standard, Parser};
51+
use crate::standard::VHDLStandard;
4552

4653
#[test]
4754
fn simple_components() {
@@ -95,4 +102,40 @@ end component;
95102
",
96103
));
97104
}
105+
106+
#[test]
107+
fn component_end_without_keyword_vhdl2019() {
108+
// `end;` without the trailing `component` keyword is valid from VHDL-2019 onwards.
109+
insta::assert_snapshot!(to_test_text_with_standard(
110+
VHDLStandard::VHDL2019,
111+
Parser::component_declaration,
112+
"\
113+
component foo is
114+
end;
115+
"
116+
));
117+
// Optional identifier after `end` is also accepted.
118+
insta::assert_snapshot!(to_test_text_with_standard(
119+
VHDLStandard::VHDL2019,
120+
Parser::component_declaration,
121+
"\
122+
component foo is
123+
end foo;
124+
"
125+
));
126+
}
127+
128+
#[test]
129+
fn component_end_without_keyword_is_error_before_vhdl2019() {
130+
// Omitting the trailing `component` keyword must produce a diagnostic under VHDL-2008.
131+
let (_, diagnostics) = parse_syntax_with_standard(
132+
VHDLStandard::VHDL2008,
133+
"component foo is\nend;\n".bytes(),
134+
Parser::component_declaration,
135+
);
136+
assert!(
137+
!diagnostics.is_empty(),
138+
"expected a diagnostic for missing 'component' keyword under VHDL-2008"
139+
);
140+
}
98141
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
source: vhdl_syntax/src/parser/productions/component_declaration.rs
3+
expression: "to_test_text_with_standard(VHDLStandard::VHDL2019,\nParser::component_declaration, \"\\\ncomponent foo is\nend foo;\n\")"
4+
---
5+
ComponentDeclaration
6+
ComponentDeclarationPreamble
7+
Keyword(Component)
8+
Identifier 'foo'
9+
Keyword(Is)
10+
ComponentDeclarationEpilogue
11+
Keyword(End)
12+
Identifier 'foo'
13+
SemiColon
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
source: vhdl_syntax/src/parser/productions/component_declaration.rs
3+
expression: "to_test_text_with_standard(VHDLStandard::VHDL2019,\nParser::component_declaration, \"\\\ncomponent foo is\nend;\n\")"
4+
---
5+
ComponentDeclaration
6+
ComponentDeclarationPreamble
7+
Keyword(Component)
8+
Identifier 'foo'
9+
Keyword(Is)
10+
ComponentDeclarationEpilogue
11+
Keyword(End)
12+
SemiColon

vhdl_syntax/src/parser/test_utils.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,23 @@
44
//
55
// Copyright (c) 2025, Lukas Scheller lukasscheller@icloud.com
66

7-
use crate::parser::{parse_syntax, Parser};
7+
use crate::parser::{parse_syntax, parse_syntax_with_standard, Parser};
8+
use crate::standard::VHDLStandard;
89

910
/// Returns the AST text for snapshot assertions.
1011
pub fn to_test_text(func: impl FnOnce(&mut Parser), input: &str) -> String {
1112
let (entity, diagnostics) = parse_syntax(input, func);
1213
assert!(diagnostics.is_empty(), "got diagnostics: {:?}", diagnostics);
1314
entity.test_text()
1415
}
16+
17+
/// Returns the AST text for snapshot assertions, tokenizing and parsing under `standard`.
18+
pub fn to_test_text_with_standard(
19+
standard: VHDLStandard,
20+
func: impl FnOnce(&mut Parser),
21+
input: &str,
22+
) -> String {
23+
let (entity, diagnostics) = parse_syntax_with_standard(standard, input.bytes(), func);
24+
assert!(diagnostics.is_empty(), "got diagnostics: {:?}", diagnostics);
25+
entity.test_text()
26+
}

vhdl_syntax/src/standard.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
3+
// You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) 2024, Lukas Scheller lukasscheller@icloud.com
6+
7+
//! VHDL standard version identification and comparison.
8+
//!
9+
//! This module provides the [`VHDLStandard`] enum to represent all supported VHDL versions
10+
//! and methods to work with standards.
11+
12+
use std::fmt;
13+
14+
/// VHDL standard version.
15+
///
16+
/// Represents different VHDL versions.
17+
/// As default, VHDL2008 is chosen.
18+
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
19+
#[non_exhaustive]
20+
pub enum VHDLStandard {
21+
VHDL1987,
22+
VHDL1993,
23+
VHDL2000,
24+
VHDL2002,
25+
#[default]
26+
VHDL2008,
27+
VHDL2019,
28+
}
29+
30+
impl VHDLStandard {
31+
/// All supported VHDL standards in order.
32+
pub const ALL: [Self; 6] = [
33+
Self::VHDL1987,
34+
Self::VHDL1993,
35+
Self::VHDL2000,
36+
Self::VHDL2002,
37+
Self::VHDL2008,
38+
Self::VHDL2019,
39+
];
40+
41+
/// Returns the standard as a string.
42+
pub const fn as_str(self) -> &'static str {
43+
match self {
44+
Self::VHDL1987 => "1987",
45+
Self::VHDL1993 => "1993",
46+
Self::VHDL2000 => "2000",
47+
Self::VHDL2002 => "2002",
48+
Self::VHDL2008 => "2008",
49+
Self::VHDL2019 => "2019",
50+
}
51+
}
52+
53+
/// Returns the year of the standard.
54+
pub const fn year(self) -> u16 {
55+
match self {
56+
Self::VHDL1987 => 1987,
57+
Self::VHDL1993 => 1993,
58+
Self::VHDL2000 => 2000,
59+
Self::VHDL2002 => 2002,
60+
Self::VHDL2008 => 2008,
61+
Self::VHDL2019 => 2019,
62+
}
63+
}
64+
65+
/// Returns true if this standard is at or newer than `other`.
66+
pub fn is_at_least(self, other: Self) -> bool {
67+
self >= other
68+
}
69+
70+
/// Returns true if this standard is older than `other`.
71+
pub fn is_before(self, other: Self) -> bool {
72+
self < other
73+
}
74+
75+
/// Returns the latest supported VHDL standard.
76+
pub fn latest() -> VHDLStandard {
77+
Self::VHDL2019
78+
}
79+
}
80+
81+
#[test]
82+
fn order_of_standards() {
83+
assert!(VHDLStandard::VHDL2008 > VHDLStandard::VHDL1993);
84+
}
85+
86+
/// Error returned when parsing an invalid VHDL standard string.
87+
#[derive(Debug, Clone)]
88+
pub struct ParseVHDLStandardError;
89+
90+
impl std::fmt::Display for ParseVHDLStandardError {
91+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92+
write!(f, "invalid VHDL standard")
93+
}
94+
}
95+
96+
impl std::error::Error for ParseVHDLStandardError {}
97+
98+
impl TryFrom<&str> for VHDLStandard {
99+
type Error = ParseVHDLStandardError;
100+
101+
fn try_from(value: &str) -> Result<Self, Self::Error> {
102+
use VHDLStandard::*;
103+
Ok(match value {
104+
"1987" | "87" => VHDL1987,
105+
"1993" | "93" => VHDL1993,
106+
"2000" | "00" => VHDL2000,
107+
"2002" | "02" => VHDL2002,
108+
"2008" | "08" => VHDL2008,
109+
"2019" | "19" => VHDL2019,
110+
_ => return Err(ParseVHDLStandardError),
111+
})
112+
}
113+
}
114+
115+
impl AsRef<str> for VHDLStandard {
116+
fn as_ref(&self) -> &str {
117+
self.as_str()
118+
}
119+
}
120+
121+
impl fmt::Display for VHDLStandard {
122+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123+
f.write_str(self.as_str())
124+
}
125+
}

0 commit comments

Comments
 (0)