Background: HRT’s Hardware Tooling Stack
At HRT, pushing technical boundaries often means moving beyond the constraints of standard software. Some of the ways we accomplish this are by deploying FPGA and ASIC solutions which allow us to reach latency and throughput levels unattainable with general-purpose CPUs. We build the custom logic needed for these platforms using SystemVerilog.
The hardware workflow relies on simulators to test the designs and synthesizers to ‘compile’ designs onto hardware. Our workflow uses a mix of open source and proprietary tools. Historically, most of these tools have been costly and proprietary – in the past we even had to pay for software compilers. While open source tooling for software has evolved and matured quite a bit over the last few decades, open source hardware tooling has developed at a slower pace. This has led to many issues:
- Limited simulator licences make CI nearly impossible
- No free and widely used linter, formatter, language server, etc.
- Different proprietary tools accept vastly different language constructs, resulting in many tool-dependent macros to achieve the same thing
We’re eager to address these issues by adopting, improving upon, and adding to the open source hardware tools. For Verilator, this means bug fixes and improvements to the open source simulator. For Cocotb, this means actively maintaining and improving that testing library, and notably increasing the parity between Verilator and proprietary simulators such that the same tests work on both simulators. Utilizing these tools allows our hardware development flow to behave more like a cohesive part of our overall software team, and also allows us to use the same build and test tools for efficient CI and share testing workflows.
It may seem counterintuitive for a trading firm, typically protective of its intellectual property, to contribute to and publish open-source projects. However, we believe it is vital to the long term success of software projects to provide extra validation, opinions, and contributions that move these projects forward. We’re also very happy to lower the barrier of entry for hardware development and improve the lives of hardware developers around the world.
Proprietary tools still have their place in our workflow. For example, proprietary simulators tend to have faster incremental builds, which many hardware designers need to utilize to achieve a quick iteration loop. Our workflow that we’ve developed allows hardware designers to simply flip a command line switch to run the same exact tests on either Verilator or a proprietary simulator. This allows Verilator to catch CI regressions quickly by running thousands of tests in parallel, while also allowing for developers to use a proprietary simulator for rapid debugging and testing of a design.
What is a language server?
The project we built and released is a language server, which is a tool that provides intelligent code navigation and instant feedback into popular editors like VS Code, Vim, and Emacs for a particular language. It’s a protocol based on the VSCode api, allowing the server to be written in any language, not just typescript. The server is based on the Slang project, which is a SystemVerilog frontend and linter. Slang was created by our own Mike Polopolski, and he’s continued to develop features and maintain that project. According to the ChipsAlliance test suite, Slang is the fastest and most compliant SystemVerilog frontend, which in hardware compilers is the engine that parses code, checks for errors, and builds the design hierarchy.
Why build a language server?
As part of my role, I need to read and understand parts of our hardware codebase. It irked me (and others on my team) every time I had to scour our codebase for declarations when seeking to understand a piece of code. I couldn’t even imagine how much time our team would save if we had proper editor features like we did in other languages, where a simple cmd+click would bring you to a symbol definition.
I started exploring this space by contributing to the most popular vscode extension at the time, first by adding slang linting. This closed a critical feedback loop in our development: users could now immediately see when they misspelled a variable or missed a semicolon, rather than having to switch to a terminal and fix those after running a build. I continued with contributions to the vscode extension, adding cross-file goto-definition and completion support. However, due to limitations in the underlying parser, I hit a wall in developing new features. In order to progress, we needed to build a proper language server using slang, which would be faster, more accurate, and work on any editor.
Luckily, at HRT we have the ability to work on greenfield projects through a program called “Surge”, in which groups of engineers choose to come together to explore and build new projects. Both our FPGA team and the Slang library were first created as Surge projects many years ago. Last year, we created slang-server during Surge, and we’re proud to have recently open sourced it.
In the rest of this article we’re going to explore some technical aspects of the server. First we’re going to explore the constructs that the Slang library provides and how we used those constructs to implement some core language features. Then, we’re going to look at optimizations that were made for the indexer’s latency and memory usage, which is a critical part of the language server that collects workspace-wide symbols when the editor is opened.
What the Slang Library Provides
Slang works in two main passes: parsing and elaboration.
Phase 1: Parsing
The first phase of the process is Parsing, composed of Lexing, Preprocessing and Parsing. Lexing breaks the raw text file into individual “words” or tokens. “Trivia” such as comments, whitespace, and tool pragmas are attached to the token that immediately follows them. Next, Preprocessing runs a preprocessor similar to those found in C and C++. This step expands macros, follows ifdef statements, and expands include directives. Finally, the main Parsing step arranges all of these processed tokens into a syntax tree, which is a hierarchical structure of syntax nodes, with each node containing tokens and other syntax nodes.
Phase 2: Elaboration
Slang walks through the set of syntax trees starting at a top level module. While doing so it calculates parameter values (e.g., “This RAM has a width of 64 bits”) and connects module instances to one another to form the final design. Since modules can be parameterized like C++ templates, Slang can’t just read the code once; it has to “build” the design to see what it actually looks like, and check for issues like width mismatches in each parameterization. During this process it converts the syntax tree into symbols – abstract objects that represent the definitions of your modules, variables, and functions.
When using the slang binary (the linter), it runs this entire process to print diagnostics (errors and warnings) to your terminal. The slang-server, on the other hand, presents these diagnostics directly to an IDE over the Language Server Protocol, providing useful info to users as they type.
Here is an illustration of some of the information that we get from Parsing and Elaboration, created with Slang’s --cst-json and --ast-json commands.
module Comparator (
input logic [3:0] count,
input logic [3:0] target,
output logic match
);
// always_comb automatically triggers whenever 'count' or 'target' changes
always_comb begin
if (count == target)
match = 1'b1;
else
match = 1'b0;
end
endmodule
module CounterTop (
input logic clk,
input logic rst_n, // Active-low reset
input logic [3:0] limit,
output logic at_limit
);
logic [3:0] current_count;
// always_ff describes sequential logic (flip-flops)
// It requires a sensitivity list, usually the clock edge
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_count <= 4'b0000;
end else begin
current_count <= current_count + 1'b1;
end
end
// Instantiate the combinational module
Comparator comp_inst (
.count(current_count),
.target(limit),
.match(at_limit)
);
endmodule{
"syntaxTrees": [
{
"kind": "SyntaxTree",
"root": {
"kind": "CompilationUnit",
"members": [
{
"kind": "ModuleDeclaration",
"header": {
"kind": "ModuleHeader",
"moduleKeyword": {
"kind": "ModuleKeyword",
"text": "module"
},
"name": {
"kind": "Identifier",
"text": "Comparator",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"ports": {
"kind": "AnsiPortList",
"openParen": {
"kind": "OpenParenthesis",
"text": "(",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"ports": [
{
"kind": "ImplicitAnsiPort",
"header": {
"kind": "VariablePortHeader",
"direction": {
"kind": "InputKeyword",
"text": "input",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dataType": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"dimensions": [
{
"kind": "VariableDimension",
"openBracket": {
"kind": "OpenBracket",
"text": "[",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"specifier": {
"kind": "RangeDimensionSpecifier",
"selector": {
"kind": "SimpleRangeSelect",
"left": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "3"
}
},
"range": {
"kind": "Colon",
"text": ":"
},
"right": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "0"
}
}
}
},
"closeBracket": {
"kind": "CloseBracket",
"text": "]"
}
}
]
}
},
"declarator": {
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "count",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
{
"kind": "Comma",
"text": ","
},
{
"kind": "ImplicitAnsiPort",
"header": {
"kind": "VariablePortHeader",
"direction": {
"kind": "InputKeyword",
"text": "input",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dataType": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"dimensions": [
{
"kind": "VariableDimension",
"openBracket": {
"kind": "OpenBracket",
"text": "[",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"specifier": {
"kind": "RangeDimensionSpecifier",
"selector": {
"kind": "SimpleRangeSelect",
"left": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "3"
}
},
"range": {
"kind": "Colon",
"text": ":"
},
"right": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "0"
}
}
}
},
"closeBracket": {
"kind": "CloseBracket",
"text": "]"
}
}
]
}
},
"declarator": {
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "target",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
{
"kind": "Comma",
"text": ","
},
{
"kind": "ImplicitAnsiPort",
"header": {
"kind": "VariablePortHeader",
"direction": {
"kind": "OutputKeyword",
"text": "output",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dataType": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
"declarator": {
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "match",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
}
],
"closeParen": {
"kind": "CloseParenthesis",
"text": ")",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
}
]
}
},
"semi": {
"kind": "Semicolon",
"text": ";"
}
},
"members": [
{
"kind": "AlwaysCombBlock",
"keyword": {
"kind": "AlwaysCombKeyword",
"text": "always_comb",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
},
{
"kind": "LineComment",
"text": "// always_comb automatically triggers whenever 'count' or 'target' changes"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"statement": {
"kind": "SequentialBlockStatement",
"begin": {
"kind": "BeginKeyword",
"text": "begin",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"items": [
{
"kind": "ConditionalStatement",
"ifKeyword": {
"kind": "IfKeyword",
"text": "if",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"openParen": {
"kind": "OpenParenthesis",
"text": "(",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"predicate": {
"kind": "ConditionalPredicate",
"conditions": [
{
"kind": "ConditionalPattern",
"expr": {
"kind": "EqualityExpression",
"left": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "count"
}
},
"operatorToken": {
"kind": "DoubleEquals",
"text": "==",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"right": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "target",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
}
}
]
},
"closeParen": {
"kind": "CloseParenthesis",
"text": ")"
},
"statement": {
"kind": "ExpressionStatement",
"expr": {
"kind": "AssignmentExpression",
"left": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "match",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
},
"operatorToken": {
"kind": "Equals",
"text": "=",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"right": {
"kind": "IntegerVectorExpression",
"size": {
"kind": "IntegerLiteral",
"text": "1",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"base": {
"kind": "IntegerBase",
"text": "'b"
},
"value": {
"kind": "IntegerLiteral",
"text": "1"
}
}
},
"semi": {
"kind": "Semicolon",
"text": ";"
}
},
"elseClause": {
"kind": "ElseClause",
"elseKeyword": {
"kind": "ElseKeyword",
"text": "else",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"clause": {
"kind": "ExpressionStatement",
"expr": {
"kind": "AssignmentExpression",
"left": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "match",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
},
"operatorToken": {
"kind": "Equals",
"text": "=",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"right": {
"kind": "IntegerVectorExpression",
"size": {
"kind": "IntegerLiteral",
"text": "1",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"base": {
"kind": "IntegerBase",
"text": "'b"
},
"value": {
"kind": "IntegerLiteral",
"text": "0"
}
}
},
"semi": {
"kind": "Semicolon",
"text": ";"
}
}
}
}
],
"end": {
"kind": "EndKeyword",
"text": "end",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
}
}
],
"endmodule": {
"kind": "EndModuleKeyword",
"text": "endmodule",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "EndOfLine",
"text": "\n"
}
]
}
},
{
"kind": "ModuleDeclaration",
"header": {
"kind": "ModuleHeader",
"moduleKeyword": {
"kind": "ModuleKeyword",
"text": "module",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "EndOfLine",
"text": "\n"
}
]
},
"name": {
"kind": "Identifier",
"text": "CounterTop",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"ports": {
"kind": "AnsiPortList",
"openParen": {
"kind": "OpenParenthesis",
"text": "(",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"ports": [
{
"kind": "ImplicitAnsiPort",
"header": {
"kind": "VariablePortHeader",
"direction": {
"kind": "InputKeyword",
"text": "input",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dataType": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
"declarator": {
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "clk",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
{
"kind": "Comma",
"text": ","
},
{
"kind": "ImplicitAnsiPort",
"header": {
"kind": "VariablePortHeader",
"direction": {
"kind": "InputKeyword",
"text": "input",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dataType": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
"declarator": {
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "rst_n",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
{
"kind": "Comma",
"text": ","
},
{
"kind": "ImplicitAnsiPort",
"header": {
"kind": "VariablePortHeader",
"direction": {
"kind": "InputKeyword",
"text": "input",
"trivia": [
{
"kind": "Whitespace",
"text": " "
},
{
"kind": "LineComment",
"text": "// Active-low reset"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dataType": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"dimensions": [
{
"kind": "VariableDimension",
"openBracket": {
"kind": "OpenBracket",
"text": "[",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"specifier": {
"kind": "RangeDimensionSpecifier",
"selector": {
"kind": "SimpleRangeSelect",
"left": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "3"
}
},
"range": {
"kind": "Colon",
"text": ":"
},
"right": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "0"
}
}
}
},
"closeBracket": {
"kind": "CloseBracket",
"text": "]"
}
}
]
}
},
"declarator": {
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "limit",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
{
"kind": "Comma",
"text": ","
},
{
"kind": "ImplicitAnsiPort",
"header": {
"kind": "VariablePortHeader",
"direction": {
"kind": "OutputKeyword",
"text": "output",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dataType": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
"declarator": {
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "at_limit",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
}
],
"closeParen": {
"kind": "CloseParenthesis",
"text": ")",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
}
]
}
},
"semi": {
"kind": "Semicolon",
"text": ";"
}
},
"members": [
{
"kind": "DataDeclaration",
"type": {
"kind": "LogicType",
"keyword": {
"kind": "LogicKeyword",
"text": "logic",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"dimensions": [
{
"kind": "VariableDimension",
"openBracket": {
"kind": "OpenBracket",
"text": "[",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"specifier": {
"kind": "RangeDimensionSpecifier",
"selector": {
"kind": "SimpleRangeSelect",
"left": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "3"
}
},
"range": {
"kind": "Colon",
"text": ":"
},
"right": {
"kind": "IntegerLiteralExpression",
"literal": {
"kind": "IntegerLiteral",
"text": "0"
}
}
}
},
"closeBracket": {
"kind": "CloseBracket",
"text": "]"
}
}
]
},
"declarators": [
{
"kind": "Declarator",
"name": {
"kind": "Identifier",
"text": "current_count",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
],
"semi": {
"kind": "Semicolon",
"text": ";"
}
},
{
"kind": "AlwaysFFBlock",
"keyword": {
"kind": "AlwaysFFKeyword",
"text": "always_ff",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
},
{
"kind": "LineComment",
"text": "// always_ff describes sequential logic (flip-flops)"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
},
{
"kind": "LineComment",
"text": "// It requires a sensitivity list, usually the clock edge"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"statement": {
"kind": "TimingControlStatement",
"timingControl": {
"kind": "EventControlWithExpression",
"at": {
"kind": "At",
"text": "@",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"expr": {
"kind": "ParenthesizedEventExpression",
"openParen": {
"kind": "OpenParenthesis",
"text": "("
},
"expr": {
"kind": "BinaryEventExpression",
"left": {
"kind": "SignalEventExpression",
"edge": {
"kind": "PosEdgeKeyword",
"text": "posedge"
},
"expr": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "clk",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
},
"operatorToken": {
"kind": "OrKeyword",
"text": "or",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"right": {
"kind": "SignalEventExpression",
"edge": {
"kind": "NegEdgeKeyword",
"text": "negedge",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"expr": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "rst_n",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
}
}
},
"closeParen": {
"kind": "CloseParenthesis",
"text": ")"
}
}
},
"statement": {
"kind": "SequentialBlockStatement",
"begin": {
"kind": "BeginKeyword",
"text": "begin",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"items": [
{
"kind": "ConditionalStatement",
"ifKeyword": {
"kind": "IfKeyword",
"text": "if",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"openParen": {
"kind": "OpenParenthesis",
"text": "(",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"predicate": {
"kind": "ConditionalPredicate",
"conditions": [
{
"kind": "ConditionalPattern",
"expr": {
"kind": "UnaryLogicalNotExpression",
"operatorToken": {
"kind": "Exclamation",
"text": "!"
},
"operand": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "rst_n"
}
}
}
}
]
},
"closeParen": {
"kind": "CloseParenthesis",
"text": ")"
},
"statement": {
"kind": "SequentialBlockStatement",
"begin": {
"kind": "BeginKeyword",
"text": "begin",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"items": [
{
"kind": "ExpressionStatement",
"expr": {
"kind": "NonblockingAssignmentExpression",
"left": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "current_count",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
},
"operatorToken": {
"kind": "LessThanEquals",
"text": "<=",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"right": {
"kind": "IntegerVectorExpression",
"size": {
"kind": "IntegerLiteral",
"text": "4",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"base": {
"kind": "IntegerBase",
"text": "'b"
},
"value": {
"kind": "IntegerLiteral",
"text": "0000"
}
}
},
"semi": {
"kind": "Semicolon",
"text": ";"
}
}
],
"end": {
"kind": "EndKeyword",
"text": "end",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
},
"elseClause": {
"kind": "ElseClause",
"elseKeyword": {
"kind": "ElseKeyword",
"text": "else",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"clause": {
"kind": "SequentialBlockStatement",
"begin": {
"kind": "BeginKeyword",
"text": "begin",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"items": [
{
"kind": "ExpressionStatement",
"expr": {
"kind": "NonblockingAssignmentExpression",
"left": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "current_count",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
},
"operatorToken": {
"kind": "LessThanEquals",
"text": "<=",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"right": {
"kind": "AddExpression",
"left": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "current_count",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
},
"operatorToken": {
"kind": "Plus",
"text": "+",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"right": {
"kind": "IntegerVectorExpression",
"size": {
"kind": "IntegerLiteral",
"text": "1",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"base": {
"kind": "IntegerBase",
"text": "'b"
},
"value": {
"kind": "IntegerLiteral",
"text": "1"
}
}
}
},
"semi": {
"kind": "Semicolon",
"text": ";"
}
}
],
"end": {
"kind": "EndKeyword",
"text": "end",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
}
}
}
],
"end": {
"kind": "EndKeyword",
"text": "end",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
}
}
},
{
"kind": "HierarchyInstantiation",
"type": {
"kind": "Identifier",
"text": "Comparator",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
},
{
"kind": "LineComment",
"text": "// Instantiate the combinational module"
},
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"instances": [
{
"kind": "HierarchicalInstance",
"decl": {
"kind": "InstanceName",
"name": {
"kind": "Identifier",
"text": "comp_inst",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
}
},
"openParen": {
"kind": "OpenParenthesis",
"text": "(",
"trivia": [
{
"kind": "Whitespace",
"text": " "
}
]
},
"connections": [
{
"kind": "NamedPortConnection",
"dot": {
"kind": "Dot",
"text": ".",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"name": {
"kind": "Identifier",
"text": "count"
},
"openParen": {
"kind": "OpenParenthesis",
"text": "("
},
"expr": {
"kind": "SimplePropertyExpr",
"expr": {
"kind": "SimpleSequenceExpr",
"expr": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "current_count"
}
}
}
},
"closeParen": {
"kind": "CloseParenthesis",
"text": ")"
}
},
{
"kind": "Comma",
"text": ","
},
{
"kind": "NamedPortConnection",
"dot": {
"kind": "Dot",
"text": ".",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"name": {
"kind": "Identifier",
"text": "target"
},
"openParen": {
"kind": "OpenParenthesis",
"text": "("
},
"expr": {
"kind": "SimplePropertyExpr",
"expr": {
"kind": "SimpleSequenceExpr",
"expr": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "limit"
}
}
}
},
"closeParen": {
"kind": "CloseParenthesis",
"text": ")"
}
},
{
"kind": "Comma",
"text": ","
},
{
"kind": "NamedPortConnection",
"dot": {
"kind": "Dot",
"text": ".",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
},
"name": {
"kind": "Identifier",
"text": "match"
},
"openParen": {
"kind": "OpenParenthesis",
"text": "("
},
"expr": {
"kind": "SimplePropertyExpr",
"expr": {
"kind": "SimpleSequenceExpr",
"expr": {
"kind": "IdentifierName",
"identifier": {
"kind": "Identifier",
"text": "at_limit"
}
}
}
},
"closeParen": {
"kind": "CloseParenthesis",
"text": ")"
}
}
],
"closeParen": {
"kind": "CloseParenthesis",
"text": ")",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "Whitespace",
"text": " "
}
]
}
}
],
"semi": {
"kind": "Semicolon",
"text": ";"
}
}
],
"endmodule": {
"kind": "EndModuleKeyword",
"text": "endmodule",
"trivia": [
{
"kind": "EndOfLine",
"text": "\n"
},
{
"kind": "EndOfLine",
"text": "\n"
}
]
}
}
]
}
}
]
}{
"design": {
"name": "$root",
"kind": "Root",
"addr": 6308034860160,
"members": [
{
"name": "",
"kind": "CompilationUnit",
"addr": 6308035446328
},
{
"name": "CounterTop",
"kind": "Instance",
"addr": 6308035447208,
"body": {
"name": "CounterTop",
"kind": "InstanceBody",
"addr": 6308035446624,
"members": [
{
"name": "clk",
"kind": "Port",
"addr": 6308035447352,
"type": "logic",
"direction": "In",
"internalSymbol": "6308035447480 clk"
},
{
"name": "clk",
"kind": "Variable",
"addr": 6308035447480,
"type": "logic",
"lifetime": "Static"
},
{
"name": "rst_n",
"kind": "Port",
"addr": 6308035447624,
"type": "logic",
"direction": "In",
"internalSymbol": "6308035447752 rst_n"
},
{
"name": "rst_n",
"kind": "Variable",
"addr": 6308035447752,
"type": "logic",
"lifetime": "Static"
},
{
"name": "limit",
"kind": "Port",
"addr": 6308035447896,
"type": "logic[3:0]",
"direction": "In",
"internalSymbol": "6308035448024 limit"
},
{
"name": "limit",
"kind": "Variable",
"addr": 6308035448024,
"type": "logic[3:0]",
"lifetime": "Static"
},
{
"name": "at_limit",
"kind": "Port",
"addr": 6308035448168,
"type": "logic",
"direction": "Out",
"internalSymbol": "6308035448296 at_limit"
},
{
"name": "at_limit",
"kind": "Variable",
"addr": 6308035448296,
"type": "logic",
"lifetime": "Static"
},
{
"name": "current_count",
"kind": "Variable",
"addr": 6308035446880,
"type": "logic[3:0]",
"lifetime": "Static"
},
{
"name": "",
"kind": "ProceduralBlock",
"addr": 6308035447024,
"procedureKind": "AlwaysFF",
"body": {
"kind": "Timed",
"timing": {
"kind": "EventList",
"events": [
{
"kind": "SignalEvent",
"expr": {
"kind": "NamedValue",
"type": "logic",
"symbol": "6308035447480 clk"
},
"edge": "PosEdge"
},
{
"kind": "SignalEvent",
"expr": {
"kind": "NamedValue",
"type": "logic",
"symbol": "6308035447752 rst_n"
},
"edge": "NegEdge"
}
]
},
"stmt": {
"kind": "Block",
"blockKind": "Sequential",
"body": {
"kind": "Conditional",
"conditions": [
{
"expr": {
"kind": "UnaryOp",
"type": "logic",
"op": "LogicalNot",
"operand": {
"kind": "NamedValue",
"type": "logic",
"symbol": "6308035447752 rst_n"
}
}
}
],
"check": "None",
"ifTrue": {
"kind": "Block",
"blockKind": "Sequential",
"body": {
"kind": "ExpressionStatement",
"expr": {
"kind": "Assignment",
"type": "logic[3:0]",
"left": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "6308035446880 current_count"
},
"right": {
"kind": "Conversion",
"type": "logic[3:0]",
"operand": {
"kind": "IntegerLiteral",
"type": "bit[3:0]",
"value": "4'b0",
"constant": "4'b0"
},
"constant": "4'b0"
},
"isNonBlocking": true
}
}
},
"ifFalse": {
"kind": "Block",
"blockKind": "Sequential",
"body": {
"kind": "ExpressionStatement",
"expr": {
"kind": "Assignment",
"type": "logic[3:0]",
"left": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "6308035446880 current_count"
},
"right": {
"kind": "BinaryOp",
"type": "logic[3:0]",
"op": "Add",
"left": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "6308035446880 current_count"
},
"right": {
"kind": "Conversion",
"type": "logic[3:0]",
"operand": {
"kind": "IntegerLiteral",
"type": "bit[0:0]",
"value": "1'b1",
"constant": "1'b1"
},
"constant": "4'b1"
}
},
"isNonBlocking": true
}
}
}
}
}
}
},
{
"name": "comp_inst",
"kind": "Instance",
"addr": 6308035448472,
"body": {
"name": "Comparator",
"kind": "InstanceBody",
"addr": 6308035448608,
"members": [
{
"name": "count",
"kind": "Port",
"addr": 6308035451440,
"type": "logic[3:0]",
"direction": "In",
"internalSymbol": "6308035451568 count"
},
{
"name": "count",
"kind": "Variable",
"addr": 6308035451568,
"type": "logic[3:0]",
"lifetime": "Static"
},
{
"name": "target",
"kind": "Port",
"addr": 6308035451712,
"type": "logic[3:0]",
"direction": "In",
"internalSymbol": "6308035451840 target"
},
{
"name": "target",
"kind": "Variable",
"addr": 6308035451840,
"type": "logic[3:0]",
"lifetime": "Static"
},
{
"name": "match",
"kind": "Port",
"addr": 6308035451984,
"type": "logic",
"direction": "Out",
"internalSymbol": "6308035452112 match"
},
{
"name": "match",
"kind": "Variable",
"addr": 6308035452112,
"type": "logic",
"lifetime": "Static"
},
{
"name": "",
"kind": "ProceduralBlock",
"addr": 6308035448920,
"procedureKind": "AlwaysComb",
"body": {
"kind": "Block",
"blockKind": "Sequential",
"body": {
"kind": "Conditional",
"conditions": [
{
"expr": {
"kind": "BinaryOp",
"type": "logic",
"op": "Equality",
"left": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "6308035451568 count"
},
"right": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "6308035451840 target"
}
}
}
],
"check": "None",
"ifTrue": {
"kind": "ExpressionStatement",
"expr": {
"kind": "Assignment",
"type": "logic",
"left": {
"kind": "NamedValue",
"type": "logic",
"symbol": "6308035452112 match"
},
"right": {
"kind": "Conversion",
"type": "logic",
"operand": {
"kind": "IntegerLiteral",
"type": "bit[0:0]",
"value": "1'b1",
"constant": "1'b1"
},
"constant": "1'b1"
},
"isNonBlocking": false
}
},
"ifFalse": {
"kind": "ExpressionStatement",
"expr": {
"kind": "Assignment",
"type": "logic",
"left": {
"kind": "NamedValue",
"type": "logic",
"symbol": "6308035452112 match"
},
"right": {
"kind": "Conversion",
"type": "logic",
"operand": {
"kind": "IntegerLiteral",
"type": "bit[0:0]",
"value": "1'b0",
"constant": "1'b0"
},
"constant": "1'b0"
},
"isNonBlocking": false
}
}
}
}
}
],
"definition": "6308031980544 Comparator"
},
"connections": [
{
"port": {
"name": "count",
"kind": "Port",
"addr": 6308035451440,
"type": "logic[3:0]",
"direction": "In",
"internalSymbol": "6308035451568 count"
},
"expr": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "6308035446880 current_count"
}
},
{
"port": {
"name": "target",
"kind": "Port",
"addr": 6308035451712,
"type": "logic[3:0]",
"direction": "In",
"internalSymbol": "6308035451840 target"
},
"expr": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "6308035448024 limit"
}
},
{
"port": {
"name": "match",
"kind": "Port",
"addr": 6308035451984,
"type": "logic",
"direction": "Out",
"internalSymbol": "6308035452112 match"
},
"expr": {
"kind": "Assignment",
"type": "logic",
"left": {
"kind": "NamedValue",
"type": "logic",
"symbol": "6308035448296 at_limit"
},
"right": {
"kind": "EmptyArgument",
"type": "logic"
},
"isNonBlocking": false
}
}
]
}
],
"definition": "6308031981312 CounterTop"
},
"connections": [
]
}
]
},
"definitions": [
{
"name": "Comparator",
"kind": "Definition",
"addr": 6308031980544,
"defaultNetType": "6308034857920 wire",
"definitionKind": "Module",
"defaultLifetime": "Static",
"unconnectedDrive": "None",
"cellDefine": false
},
{
"name": "CounterTop",
"kind": "Definition",
"addr": 6308031981312,
"defaultNetType": "6308034857920 wire",
"definitionKind": "Module",
"defaultLifetime": "Static",
"unconnectedDrive": "None",
"cellDefine": false
}
]
}Implementing Goto Definition
Now let’s explore how we use this information to implement one of the most commonly used features that a language server provides: Goto Definition.
A compiler like Slang will produce symbols that reference lower and lower levels of abstraction; however, we need pointers going the other way. For example, Slang has a Compilation, which points at Symbols, which point at SyntaxNodes, which point at Tokens, which finally point at SourceLocations. When asking the language server for a definition, we need to traverse this chain in the opposite direction using what’s called a reverse index. Let’s explore how this is accomplished at each step.
SourceLocation (offset) -> Token: Binary Search
Using a binary search was chosen instead of array or hashmap for reduced memory and better startup latency when a file is opened.
Token -> Syntax: Token index
While visiting the syntax tree, we create a map of tokens to their parent syntax, since tokens in Slang don’t have parent pointers (Syntax Nodes do). Some sort of binary search on the tree itself will not work since the tree’s tokens may have come from macro expansions, so comparing those source locations would not make sense.
Token/Syntax -> Symbol:
If we’re looking at an expression:
- We can use Slang’s lookup functionality if we have a NameSyntax and a Scope object. A NameSyntax is a class of syntax nodes that look like a name, and may include things like array accesses and struct member accesses. To get this we can follow parent pointers up to a name syntax. To grab a Scope object, we create a map of syntax nodes to scopes on the initial indexing of the shallow compilation.
If we’re on a symbol declaration:
- We can visit the symbols in our file when it’s opened and index the name token of each symbol. This is used to access the symbol for go-to references and hovers, since we’re already at the symbol declaration.
If we’re using named ports/params/args/struct fields:
- We can either index on startup, or crawl back to the symbol definition and get the arguments from there.
There are plenty of other cases to think about that we won’t go into here, such as import/export statements, macro arguments, macro usages, etc. Once we have the symbol, we can get the symbol’s syntax to use in both hovers and goto-defs, and also display relevant information like type information, bitwidth, and more from the symbol.
Shallow Compilations
The “Compilation” or AST that we use only needs to provide the symbols visible in the current document, so we can use what we call a Shallow Compilation to only elaborate what we need. Most hardware tools typically require that you compile and elaborate an entire design, but we’re able to tweak Slang to provide just the information needed. The shallow compilation only adds syntax trees of directly referenced modules before elaborating. This limited depth means we can recompile the shallow compilation on every keystroke and keep up with the user, rather than performing a costly elaboration on an entire design. This has the added benefit that the user isn’t required to select a design to get language features, which some other SystemVerilog language servers require. Features for analyzing an entire design do exist in the server, but even when a design is set, the server uses a shallow compilation to keep up with the user’s typing and provide core language features on every keystroke.
Optimizing the Indexer
Now let’s explore a critical part of the language server: the indexer. This costly process runs when the language server is first started. It crawls the workspace for SystemVerilog files and makes mappings of things like module names to files that contain those modules. It’s crucial that this indexing process runs in a reasonable time so that the user isn’t left waiting when opening up their editor.
Let’s explore some optimizations that were made in the indexer.
Optimization #1: Avoid shared memory and lock contention
This code looks fairly innocent:
void populateIndexForSingleFile(const fs::path& path, Indexer::IndexedPath& dest) {
dest.path = path.string();
if (auto tree = slang::syntax::SyntaxTree::fromFile(path.string())) {
getDataFromTree(tree->get(), dest.relevantSymbols, dest.relevantMacros);
}
else {
ERROR("Error parsing file for indexing: {}", path.string());
return;
}
};However, it’s actually very inefficient if you explore what it does in more depth. The SyntaxTree::fromFile method was added for convenience in testing. It uses a global source manager, which is the class that manages the file data read from disk. For efficiency, this class ensures that each file is only read once. Also, to enable parallel parsing, this SourceManager has locks on most of its methods.
These are useful optimizations during normal usage in the slang linter, but in our use case these are hindrances since each indexing operation is completely independent. There’s no time saved by locking and checking if the file was already parsed, since each file is indexed exactly once and we don’t bother expanding included files during indexing. Realizing this, we can instead use one SourceManager per thread on the stack to greatly speed up our indexing. Before, each thread had to reach up into a shared cache level to grab this source manager and contend for the lock. Now each thread has no contention and no cache invalidation from sharing a source manager.
Optimization #2: std::unordered_map<SmallVector<>> instead of std::multimap<>
It’s possible that a top level symbol name is declared in multiple files – for example, different packages of the same name that hold different parameterizations of a design, or different versions of a module. Because of this, a multimap was initially chosen to store the module to file mapping since it can store a range of values for a key. However, it has log(n) lookup. This cost is also incurred while building the index. An unordered map is a better choice for performance, but if we store a vector in the value, that’s another malloc that will often be just one file. Slang has the exact utility for this: a SmallVector implementation. This stores a fixed amount of elements directly in the object before deferring to a normal vector, which saves some indirection. Slang and other compilers use this construct extensively to reduce extra memory allocations when the size of a list is expected to be small.
Optimization #3: Only store paths once
File paths can take up a lot of memory in our data structures, since the same path will be found in different maps. Also, hashing this string can be expensive. By interning it, or associating it with an integer id, we hash and store the string once and reuse that hash.
Optimization #4: Crawl workspace instead of globbing
Globs were initially used to specify which files to index; however, globs are expensive to compute and to crawl, and it’s tricky to specify the four common file extensions that SystemVerilog code lives in. To speed this up and simplify usage, we can crawl the directory directly instead, and allow the user to specify directories that it should skip over – for example, directories containing build artifacts or software.
These are real optimizations that were added before adding the goto-references feature, resulting in about a 10x speedup. After adding the referenced top level symbols to the indexing process, it was still about a 4x improvement, providing a meaningful impact for the users of our codebase. We tested it on the largest open source SystemVerilog repo, which comes in at 45MB of code and is composed of 3955 files. This was indexed in .41 seconds on an M1 Macbook Pro, composed of .13s crawling and .28s parsing and indexing.
Wrapping Up
HRT chooses to invest in developer efficiency and experience, and we’re proud to share these projects with the hardware community. It is this exact commitment to developer efficiency that has enabled us to scale up and trade on over 200 markets around the world while maintaining a headcount of only around 1,150 people.
If you’re a hardware designer or verification engineer looking to work on cutting-edge silicon in a modern development environment, check out our hardware roles here. Or, if you’re interested in working on similar challenging problems close to modern or custom hardware, check out our software roles here.
To try out the language server, simply install it on any VSCode based editor. There’s no longer a need to build it from source; the vscode client will now prompt to auto-install the latest binary from the releases page. Instructions for other editors can be found here.
This year during Surge, members of our FPGA team and I are making slang-format, a SystemVerilog formatter. If you’re interested in updates like the addition of the formatter, follow along on the slang-server repo.
Sources
- https://github.com/MikePopoloski/slang
- https://github.com/hudson-trading/slang-server
- http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/
Recommended Reading
