Skip to main content

logicaffeine_web/ui/components/
verilog.rs

1//! The Verilog lexer the Studio's editor colours `.v` designs by: keywords and types, sized
2//! literals (`4'd8`, `8'hFF`), `//` and `/* */` comments, strings, system tasks (`$display`),
3//! compiler directives (`` `timescale ``), operators and punctuation — every character of the
4//! source in exactly one token.
5
6use super::code_editor::{Token, TokenKind};
7
8/// Words that shape a design — its structure, control flow, ports and events — and the net and
9/// variable types it declares with, all coloured as keywords.
10const KEYWORDS: &[&str] = &[
11    "always", "always_comb", "always_ff", "always_latch", "and", "assert", "assign", "assume",
12    "automatic", "begin", "buf", "case", "casex", "casez", "cover", "deassign", "default",
13    "defparam", "disable", "edge", "else", "end", "endcase", "endfunction", "endgenerate",
14    "endmodule", "endprimitive", "endproperty", "endsequence", "endspecify", "endtable", "endtask",
15    "enum", "event", "for", "force", "forever", "fork", "function", "generate", "genvar", "if",
16    "initial", "inout", "input", "join", "localparam", "module", "nand", "negedge", "nor", "not",
17    "or", "output", "parameter", "posedge", "primitive", "property", "release", "repeat",
18    "sequence", "specify", "specparam", "struct", "table", "task", "typedef", "wait", "while",
19    "xnor", "xor", "bit", "byte", "int", "integer", "logic", "longint", "packed", "real",
20    "realtime", "reg", "shortint", "signed", "supply0", "supply1", "time", "tri", "tri0", "tri1",
21    "triand", "trior", "unsigned", "wand", "wire", "wor",
22];
23
24/// Operators, longest first so each is read whole.
25const OPERATORS: &[&str] = &[
26    "<<<", ">>>", "===", "!==", "<=", ">=", "==", "!=", "&&", "||", "<<", ">>", "~&", "~|", "~^",
27    "^~", "**", "->", "+:", "-:", "::", "+", "-", "*", "/", "%", "=", "<", ">", "!", "&", "|",
28    "^", "~", "?", ":", "@", "#",
29];
30
31const PUNCTUATION: &[char] = &['(', ')', '[', ']', '{', '}', ';', ',', '.'];
32
33/// `source` as the editor colours it, token by token; the tokens spell `source` back exactly.
34pub(crate) fn tokens(source: &str) -> Vec<Token> {
35    let mut out: Vec<Token> = Vec::new();
36    let mut at = 0;
37    while at < source.len() {
38        let rest = &source[at..];
39        let (len, kind) = next_token(rest);
40        out.push(Token { text: rest[..len].to_string(), kind });
41        at += len;
42    }
43    out
44}
45
46/// The length in bytes and kind of the token `rest` starts with.
47fn next_token(rest: &str) -> (usize, TokenKind) {
48    let first = rest.chars().next().expect("a token starts somewhere");
49    let word = |from: usize| from + rest[from..].find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '$')).unwrap_or(rest.len() - from);
50    if first.is_whitespace() {
51        return (rest.find(|c: char| !c.is_whitespace()).unwrap_or(rest.len()), TokenKind::Identifier);
52    }
53    if rest.starts_with("//") {
54        return (rest.find('\n').unwrap_or(rest.len()), TokenKind::Comment);
55    }
56    if rest.starts_with("/*") {
57        return (rest[2..].find("*/").map_or(rest.len(), |i| i + 4), TokenKind::Comment);
58    }
59    if first == '"' {
60        return (string_len(rest), TokenKind::String);
61    }
62    if (first == '$' || first == '`') && rest[1..].starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
63        return (word(1), TokenKind::Builtin);
64    }
65    if first == '\\' {
66        return (rest.find(char::is_whitespace).unwrap_or(rest.len()), TokenKind::Identifier);
67    }
68    if let Some(len) = number_len(rest) {
69        return (len, TokenKind::Number);
70    }
71    if first.is_ascii_alphabetic() || first == '_' {
72        let len = word(0);
73        let text = &rest[..len];
74        let kind = if KEYWORDS.contains(&text) { TokenKind::Keyword } else { TokenKind::Identifier };
75        return (len, kind);
76    }
77    if let Some(op) = OPERATORS.iter().find(|op| rest.starts_with(**op)) {
78        return (op.len(), TokenKind::Operator);
79    }
80    let len = first.len_utf8();
81    if PUNCTUATION.contains(&first) {
82        (len, TokenKind::Punctuation)
83    } else {
84        (len, TokenKind::Identifier)
85    }
86}
87
88/// The length of the string literal `rest` opens: to its closing quote, past escapes, or to the
89/// end of its line if it has none.
90fn string_len(rest: &str) -> usize {
91    let mut escaped = false;
92    for (i, c) in rest.char_indices().skip(1) {
93        match c {
94            '\n' => return i,
95            '"' if !escaped => return i + 1,
96            '\\' if !escaped => escaped = true,
97            _ => escaped = false,
98        }
99    }
100    rest.len()
101}
102
103/// The length of the number `rest` opens, if it opens one: a decimal (`12`, `1.5e3`), a sized
104/// or unsized based literal (`4'd8`, `'hFF`, `32'sd5`), or an unbased one (`'0`).
105fn number_len(rest: &str) -> Option<usize> {
106    let bytes = rest.as_bytes();
107    let run = |from: usize, ok: &dyn Fn(u8) -> bool| from + bytes[from..].iter().take_while(|b| ok(**b)).count();
108    let decimal = |b: u8| b.is_ascii_digit() || b == b'_';
109    let mut at = run(0, &decimal);
110    if bytes.get(at) == Some(&b'\'') {
111        let mut base = at + 1;
112        if matches!(bytes.get(base), Some(b's' | b'S')) {
113            base += 1;
114        }
115        if matches!(bytes.get(base), Some(b'b' | b'B' | b'o' | b'O' | b'd' | b'D' | b'h' | b'H')) {
116            let value = |b: u8| b.is_ascii_hexdigit() || matches!(b, b'_' | b'x' | b'X' | b'z' | b'Z' | b'?');
117            return Some(run(base + 1, &value));
118        }
119        if at == 0 && matches!(bytes.get(1), Some(b'0' | b'1' | b'x' | b'X' | b'z' | b'Z')) {
120            return Some(2);
121        }
122    }
123    if at == 0 {
124        return None;
125    }
126    if bytes.get(at) == Some(&b'.') && bytes.get(at + 1).is_some_and(u8::is_ascii_digit) {
127        at = run(at + 1, &decimal);
128    }
129    if matches!(bytes.get(at), Some(b'e' | b'E')) {
130        let sign = at + 1 + usize::from(matches!(bytes.get(at + 1), Some(b'+' | b'-')));
131        if bytes.get(sign).is_some_and(u8::is_ascii_digit) {
132            at = run(sign, &decimal);
133        }
134    }
135    Some(at)
136}
137
138#[cfg(test)]
139#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/components/verilog/tests.rs"]
140mod tests;