TL-X 2a HDL Extension Syntax Draft Specification
Languages need to be free!
Table of Contents
2.3. Example Code (TL-Verilog)
7.1. Delimited Identifier Names
7.2. Mixed-Case Identifier Names
10. Behavioral Hierarchy Scope
15. Assignment Statements/Blocks
18. Sequential Blocks (\always_comb)
19.2. HDL Signal/Structure Type Support
19.3. Assigned and Used Signals
TL-X is a set of language constructs that can extend various Hardware Description Languages (HDLs), including Verilog/SystemVerilog (as “TL-Verilog”), VHDL (as “TL-VHDL”), and System-C (as “TL-C”). The “X” in TL-X is a wildcard, referring to the target HDL. Extension features provide higher-level context for digital hardware design than those available in Register Transfer Level (RTL) languages. This context enables a concise syntax that does not sacrifice RT-Level expressibility. TL-X constructs can be processed by a “TL-X processor” to generate native code in the target HDL (without parsing the constructs of the target HDL).
The TL-X definition is evolving through voluntary contributions by members of TL-X.org, who maintain this standard. When the time is right, other standards associations such as Accellera and IEEE will be considered.
This specification is not intended for those first learning TL-X. It is specifically a reference for the language extension syntax. It does not describe the complete semantics, or meaning of the features. Other resources are available at http://tl-x.org to introduce the main concepts of TL-X. Specifically, the reader should already have an understanding of “behavioral hierarchy” (/hier), TL-X pipelines (|pipe), and “pipestages” (@1).
This specification covers TL-X syntax, as well as syntax specific to TL-Verilog, TL-VHDL, and TL-C.
TL-X syntax is intentionally rigid. Improper syntax must be reported. The interpretation and handling of improperly-formatted files is up to the discretion of the TL-X processor, except in that the improper syntax must be reported in some manner.
Here we summarize the major contributions of each version of TL-X. For complete details about previous versions and the changes from one version to the next, please consult the specifications for prior versions, available at TL-X.org.
New features in:
TL-X 2a is awarded a new major version number due to the addition of flows ($ANY). Also significant is the addition of state signals ($StateSigName).
The new features in TL-X 2a are:
A TL-X file begins with a file format line, similar to a "magic number" that identifies the TL-X language version, and target HDL used in the file.
The remainder of the file is organized in regions that are either HDL regions or TL-X regions. HDL regions contain native HDL code. TL-X regions enable the TL-X language extension. There are also HDL_plus regions that utilize the target HDL’s syntax with the ability to reference TL-X signals. These exists to ease manual conversions of HDL code to TL-X, and to provide access to native HDL features that are not accessible in TL-X regions.
A TL-X file is expected to define a module in the target HDL. It should begin with an HDL region defining the module interface and end with an HDL region terminating the module definition.
The body of the module can be defined in a single TL-X code region. Lower-level module instantiations can be made from within the TL-X code. HDL and HDL_plus code regions might be used within the module body as well to:
A TL-X region provides logic in assignment statements in the context of hierarchy, pipelines, and pipestages. This context is provided via scope lines, where each level of scope introduces indentation of three spaces.
Example TL-Verilog code below is colored based on its parse context. Contexts listed here are defined in detail throughout this specification.
\TLV_version 2a: tl-x.org
\SV
module example(
input [4:0] opcode_u101H [3:0],
input clk,
output [3:0] arith_err_u104H
);
\TLV
|pipe
// Four instructions
/inst[3:0]
@0
$valid = ...;
?$valid
@1
! $opcode[4:0] = *opcode_u101H[inst];
$add = $opcode == 5'b00000;
/* ... */
/inst[*]
?$valid
@3
$write = $opcode == 5'b11000 /* precise decode */;
@4
! *arith_err_u104H[inst] = ($add && $overflow) || ($sub && $underflow);
\SV
endmodule;
A TL-X source code file is a text file using UTF-8 character encoding. Characters fall into these mutually-exclusive types:
Additionally, we define the following character classes as collections of character types:
Non-TL-X characters are only permitted in non-TL-X contexts -- currently, HDL contexts. Their use is restricted only by external specifications or processors, such as HDL specifications and tools. Non-TL-X contexts include HDL regions, HDL_plus regions, and assignment statements outside of signal references.
Note that all TL-X characters, are in the range 0-127, and are therefore equivalent to ASCII text. Therefore, a TL-X file can be an ASCII text file, though, since Non-TL-X characters can be outside the range of ASCII characters, a legal TL-X file is not necessarily ASCII. (Future TL-X versions might allow non-ASCII TL-X characters.)
The first line of a TL-X file must identify the TL-X File Format Version and HDL Language, as well as a URL to the language specification. Currently, it must be exactly this:
For TL-Verilog: \TLV_version 1a: tl-x.org
For TL-VHDL: \TLVHDL_version 1a: tl-x.org
For TL-C: \TLC_version 1a: tl-x.org
The newline sequence used on this line defines the newline sequence for the remainder of the file.
There are three types of code regions, HDL, HDL_plus, and TL-X.
HDL regions contain native HDL code. No TL-X language extensions are accessible in these regions.
HDL regions begin with a line that begins exactly as follows, where the “/” is the line type character:
For TL-Verilog:
\SV
For TL-VHDL:
\VHDL
For TL-C
\C
There may be spaces and HDL-style comments following this region identifier.
The region ends where another code region is started, or with end-of-file.
It is expected that there will be an HDL region prior to any TL-X region that establishes the context for the TL-X code. This includes, primarily, the module interface definition. It might include other context as well. HDL regions and HDL_plus regions should not alter this context. Additionally, this region should provide the main clock. A final HDL region is expected to terminate the module definition.
HDL_plus regions are parsed as a single large block of HDL code with TL-X signal references that are relative to the default top-level TL-X scope. (As described later, this is stage 0 (@0) of pipeline “none” (|none).)
HDL_plus regions' primarily uses are:
HDL_plus regions begin with a line that begins exactly as follows, where the “\” is the line type character:
For TL-Verilog:
\SV_plus
For TL-VHDL:
\VHDL_plus
For TL-C
\C_plus
The region ends when line indentation returns to the top level (to define a new code region).
TL-X regions begin with a line that begins exactly as follows, where the “/” is the line type character:
For TL-Verilog:
\TLV
For TL-VHDL:
\TLVHDL
For TL-C
\C
The region ends when line indentation returns to the top level (to define a new code region).
Lines of code in a TLV or SV_plus region begin with a character that defines the line type. Lines with a special line type are generally ones that deserve special attention. Line type chars are:
TL-X defines a rich set of identifier types, each with their own namespace. Though cryptic at first, once these fundamental language constructs become familiar they convey meaning very succinctly, without the need for naming conventions (that are never followed consistently).
Identifiers begin with a type prefix containing zero or more symbol characters. If the next character is numeric, the identifier is a numeric identifier. If the next character is alphabetic it starts the name of a named identifier -- either a delimited identifier, or a mixed-case identifier, determined by the prefix. (Name syntax is defined in 7.1. Delimited Identifier Names and 7.2. Mixed-Case Identifier Names). If the next character is a whitespace or brace (a non-TL-X character would be illegal), terminating the identifier, the identifier is an operator, though no operators are defined in TL-X 1a. The identifier's type is defined by its syntax, which includes the prefix and the delimitation style (for delimited identifiers) or the numeric style (for numeric identifiers). Some named identifiers types are reserved for keywords, defined by TL-X in 7.4. Identifier Types. This way, keywords do not interfere with the namespace of user-defined names. There is a single syntax-to-type translation that is consistent in all contexts where identifiers are processed, defined in 7.4. Identifier Types.
A delimited identifier name is a string comprised of delimited alphanumeric tokens. A token is a caseless string of alphabetic characters optionally followed by any number of numeric characters. The first token must begin with a minimum of two alphabetic characters. There are four methods of delimitation, distinguished by case of these first two alphabetic characters. The four identifiers below have the same tokens but different delimitation style:
Most names in TL-X are delimited identifiers, including those that translate to HDL. The restriction on name syntax allows names to carry over directly to HDL with additional available HDL namespace for other generated/derived names.
Some prefixes expect mixed-case identifiers. Mixed case identifier names can contain any alpha-numeric characters of either case plus '_'. They begin with an alphabetic character (of either case).
Mixed case identifiers generally do not transfer into HDL, as doing so would consume the entire HDL namespace.
Numeric identifiers are ones whose prefix character(s) (if any) are followed by a numeric string which begins with a decimal digit. However, if that decimal digit is preceded by “+” or “-”, the “+”/“-” is taken to be part of the numeric string. To avoid ambiguity, numeric identifier prefixes must not end with “+”/”-”. Currently the numeric string consists entirely of the optional “+”/”-” followed by decimal digits, to define a decimal number.
TL-X identifier types are as follows. These are described subsequently.
Identifiers with the following prefixes utilize mixed case:
Keywords (which have syntaxes: \keywords (mixed case); and $KEYWORDS (upper case)):
Scope:
Signals:
Other:
Behavioral scope indices and signal bit indices appear in range/index expressions in one of the following formats:
[<expr>:<expr>]: Range
[{<expr>:<expr>}]: Subset range
[<expr>]: Index
[*]: Complete range as defined elsewhere
<expr> is an HDL expression. For bit ranges of assigned pipesignals, expressions must be constant across all contexts through which the assigned bits might pass, including through $ANY expressions (see Transactions ($ANY)). For behavioral scope declaration ranges, expressions must be constant across all HDL contexts in which the behavioral scope is declared (including [*] declarations). HDL expressions may contain TL-X signal references in situations where the HDL permits the use of signals. [TODO: Explain assignment for array write -- SV-specific.]
The HDL-context escape character “\” can be used to avoid special parsing of a character. For example, “\:” or “\]”.
TL-X processors may provide checking and optimization for the generated HDL if they can evaluate the expressions. For example, if only some bits of a pipesignal are required in a later pipestage, a TL-X processor could stage only those that are required.
The use of whitespace is meaningful in TL-X. A single level of scope is conveyed by indentation of three spaces (or, in the case of the first level of indentation, the line type character followed by two spaces). Tabs are forbidden in TL-X regions. (One should beware of one’s editor's auto-indentation mode.)
Using specific indentation to convey scope, rather than using begin/end delimiters enforces consistent coding style and avoids inconsistencies between indentation and scope.
Every level of scope is introduced by a scope line containing an identifier, sometimes followed immediately (no whitespace) by a range and, optionally, (conformant) whitespace and an HDL-style single- or multi-line comment.
Scope in TL-X is termed reentrant, in the sense that the flow of the code can define logic in a particular scope, leave that scope, and reenter that scope to define more logic. This is unlike HDLs, where a module is defined contiguously, and repeating a similar loop results in different loop scopes. For scopes classes that are reentrant we say that multiple lexical scopes can define a single logical scope.
Some scope identifiers are categorized as behavioral scope identifiers. Behavioral scope provides hierarchy for a TL-X model. It includes behavioral hierarchy and pipelines. The full path to a pipesignal includes all levels of behavioral scope and does not include other scope. (See 18. Pipesignal References.)
This table summarizes scope classes.
Scope | Range | Nesting Requirements | Behavioral Scope | Reentrant | Section |
Behavioral Hierarchy | none [{sub-max:sub-min}] [*] | Any | Yes | Yes | |
Pipeline | none | Exactly one required | Yes | Yes | |
Pipestage | none | Exactly one required, below pipeline | No | Yes | |
When | none | Any | No | Yes | |
Source | none | Any | No | N/A |
Behavioral hierarchy is a form of behavioral scope, and therefore provides a level of hierarchy with its own namespace. It also can enables replication of logic. Replication of the scope is specified as a range. The following forms are permitted:
The lexical range expressions for a logical behavioral scope must define a single range for the logical scope. The lexical scopes must all specify a range, or none of them may. If ranges are provided, one must specify the complete scope (form 1). Others must be identical (by string compare), or be [*], (form 4), or specify a subset range (form 3).
A behavioral hierarchy identifier must be distinct from identifiers of all parent levels of behavioral hierarchy.
Pipeline scope (|my_pipe) is a form of behavioral scope, and therefore provides a level of hierarchy with its own namespace. Pipeline scope cannot be replicated, so there can be no range expression. All logic in TL-X is defined within a pipeline. Therefore, there must be exactly one pipeline scope for every assignment statement.
All logic in TL-X is defined within the context of a pipeline and a pipestage within that pipeline. Therefore, there must be exactly one pipestage scope for every assignment statement, and that pipestage scope must be within a pipeline scope. The following forms of pipestage scope are permitted:
A “when statement” provides a condition under which the contained assignment statements produce valid results. When forms are:
The condition signal must be a single-bit (boolean) signal in the same behavioral scope as the when statement, which, when asserted, indicates validity. When not asserted, the values that would be produced by the assignments under the when scope are “invalid” and may not be consumed [the definition of consumed is under debate].
An assignment’s when scope is defined by all when scope levels within which the assignment is contained. If the condition is a pipesignal, that pipesignal’s when scope must be the same, or a subscope of that of the when statement (in addition to being within the same behavioral hierarchy level).
Source scope is provided as an aid to situations where TL-X code is generated by a preprocessor. TL-X.org is intentionally avoiding the temptation to aggressively specify new language features for every situation. Simplicity is golden, and legacy from feature bloat becomes an anchor. Many of the features that are not yet natively supported in TL-X, can be accomplished with standard macro preprocessors, such as M4. Even with a fully-featured language, it is impossible to plan for every situation, and there are always times when extending capabilities using a preprocessor is the best answer.
The primary difficulty presented by the use of a preprocessor (as with the use of a TL-X processor) is the fact that downstream tools are unaware of the source code and cannot relate error messages and the like back to the source code. To help, TL-X supports the \source and \end_source constructs:
\source <file> <line>
This is a scope line that indicates that this line and its contained TLV code came from the given source file and line number. To be precise, the line number identifies the source line that produced the "\source" line. These lines are not counted when reporting line numbers in error messages. It is expected that the line that ends this scope is the first that is no longer from the specified source file.
There can be multiple levels of \source scope, enabling multiple levels of preprocessor macro instantiation. Well-formed error messages will report all levels of instantiation.
Assignment statements and assignment blocks (or just “assignments”) are the leaves of the parse tree. They define logic. They utilize HDL syntax for logic expressions, but use TL-X signal references ($sig) in place of HDL signals. A TL-X processor need not parse the HDL expression syntax.
All assignment statements begin with a “$”, “%”, or “*” character. Assignments continue on subsequent lines with deeper indentation (any number of additional spaces of indentation; not necessarily three spaces). Assignment blocks begin on a line with a keyword that has a “\” prefix and continue on subsequent lines with a minimum of three spaces of additional indentation.
The following assignment forms are permitted:
\HDL_plus
<HDL code with pipesignal references>
\always_comb
<SystemVerilog always_comb body with pipesignal references>
The HDL-context escape character “\” can be used within assignment statements to avoid special parsing of a character. For example, “$display” in SystemVerilog context might be undesirably interpreted as a pipesignal, so “\$display” can be used for the SystemVerilog display task and “\%d” for print formatting.
The common assignment form (form 1.) has an explicit left-hand side. Signal references on the left-hand side are recognized as assigned without the need for “$$”. The line begins with “$” or “*”. If it begins with “*”, an HDL type declaration is expected as in 19.2. HDL Signal/Structure Type Support, followed by a single space, prior to the “$” of the assigned signal. “=” (“<=” for TL-VHDL) with conformant whitespace on both sides separates left-hand and right-hand sides. The left-hand and right-hand sides are interpreted as HDL syntax. “\” is an escape character that is dropped in translation and causes the next character to be interpreted as an HDL character, so it will not be interpreted as the start of a TL-X reference.
For TL-Verilog, “assign“, “always_comb“, “begin”, and “end” are not required.
The HDL_plus assignment form (form 2.) can be used for any HDL expressions. Lines of the body must be indented at least three additional spaces. The body is simply a block of HDL code that can contain TL-X signal references. It does not inherently distinguish produced signals from consumed signals, so produced signals must have a “$$” prefix.
Sequential, or always_comb blocks are supported in TL-Verilog only. They provide a code block with sequential execution semantics. They are a simple shorthand for a \SV_plus block containing an always_comb block. The TL-Verilog code:
\always_comb
expression1;
expression2;
is equivalent to:
\SV_plus
always_comb begin
expression1;
expression2;
end
16.1. References
A reference is a string of identifiers with no delimitation between identifiers. A reference refers to an element of the design -- most commonly, a pipesignal. It includes the scope of the design element, relative to the scope of the reference. Note that identifier types with no prefix characters (which don’t currently exist) are not legal in references. The same is true of operators. Signal references are used within assignment statements, defined below.
These signal types are supported within logic expressions. (See 15. Assignment Statements/Blocks.)
Pipesignals can have datatypes from the targeted HDL, either user-defined structure types or built-in HDL types. The type of a pipesignal may be declared in a type declaration statement, or along with an assignment. The HDL type is prefixed with "**". A type declaration statement may be outside of stage scope, but must be within a pipeline.
Before declaring an instance with an HDL type within TL-X context, the type must be defined within HDL context. For example,
\SV
typedef struct {
node field1;
node field2;
} struct_name;
A type declaration statement begins with an HDL type identifier, then the pipesignal, followed by a semicolon, as follows:
\TLV
|pipeline
**struct_name $object_name; // type declaration statement
The type can also be declared within a common form assignment (see 16. Common Assignment Form), as follows:
\TLV
|pipeline
@2
**struct_name $object_name.field1 = ...; // assignment statement
$object_name.field2 = ...; // assignment statement
Note: Text after $object_name above is interpreted as HDL-syntax. Since $object_name[..] would be a reference to a bit-range, $object_name\[..\] is required for some HDLs to index into an array type.
A TL-X processor must be able to determine which pipesignals are produced (aka assigned) and which are consumed (aka used). (The distinction is not necessary for HDL signal references (*HDL_sig).) The signal identifier $$sig, explicitly references $sig as assigned. This syntax must be used for all assigned signals in all assignment forms except the common form (form 1.) (see 15. Assignment Statements/Blocks).
There must generally be exactly one assignment for every pipesignal. In situations where a pipesignal must be used in multiple places as an assignment, one of those places can be chosen to represent the assignment, and the others can safely be treated as uses. Within an assignment block multiple assignments to the same signal may be used, but each must specify the same signal type.
Generally, the single assignment (or in rare cases, multiple assignments) specifies the type of the signal. The exception to this rule is HDL types, which cannot be specified within assignment blocks, and can be specified separately. (See 19.2. HDL Signal/Structure Type Support.) The type can be specified as an HDL type within common form assignments, or in any context, a bit range may be provided immediately following the assigned signal identifier (with no intervening whitespace). If no HDL type or range is specified, the signal is a single bit.
Pipesignal references are used within assignment statements. A simple signal reference is:
$sig.
This refers to a pipesignal in the same scope.
A reference to a pipesignal produced in a different scope might look like:
/scope|pipe/instr[2]<<2$addr[12:6]
The path (/cope|pipe/instr[2]) identifies the behavioral scope instances of the reference. The first identifier of the path (if the path is not empty) refers to any parent hierarchy level of the assignment's scope (each of which is required to have a unique identifier) or to a child scope. Top level scope has an implicit identifier “/top”. Subsequent path identifiers identify child scope instances. A replicated behavioral hierarchy level may be provided an index expression. If none is provided, the corresponding index of a same-named scope on the ancestry of the assignment’s scope is assumed. In other words, if /scope is replicated, /scope refers to /scope[#scope].
The right-most scope index(es) may be given as wildcards ([*]). This will reference the concatenation of all instances. This is most useful for bitwise reduction operators (& /inst[*]$bit) or for providing inputs to HDL modules that expect packed bit masks.
Assigned (left-hand, or $$sig) pipesignals have the scope of the assignment and must have an empty path.
<<2, >>3, and <>0 are examples of pipeline alignments. They are used in references to specify the numeric pipestage of the reference relative to the assignment's stage scope. For references within a pipeline, << is used to reference a pipesignal of a transaction that is behind the transaction of the assignment statement. Likewise, >> is used to reference ahead. A natural or zero alignment is assumed for references within a pipeline (into a pipeline by the same name) if no alignment is specified. For cross-pipeline references, the terms “ahead” and “behind” have less meaning, but the operators are used just the same to indicate a relative numeric stage offset, and an explicit alignment is required.
The waterfall diagram below, illustrates a reference, in stage 2, to >>1, the previous transaction, which is in stage 3.
Figure 1: Waterfall Diagram
The bit range [12:6] may be interpreted by a TL-X processor to determine what bits must be made available in the consuming stage. The TL-X processor may provide more bits than necessary (though logic synthesis typically does an excellent job of pruning unused logic). This can be expected when non-numeric HDL expressions are used in the bit range.
$RETAIN can be used as a used signal in an assignment. It translates to the single destination signal delayed by one cycle. It can be used to retain/recirculate the previous value of the signal.
An $ANY assignment statement is an assignment statement that assigns $ANY and consumes zero or more $ANY references. Such assignments provide a default assignment expression for signals in the assigned $ANY’s scope that are not explicitly assigned. There can be at most one assignment to $ANY in each behavioral scope. When a pipesignal is consumed and there is no corresponding assignment in the corresponding scope, the pipesignal is assigned according to the $ANY assignment where each occurrence of $ANY represents that pipesignal’s identifier.
$ANY assignments are used to create transaction flows. While pipesignal assignments provide nodes of a data flow graph at the pipesignal level, assignments that produce and consume $ANY create a data flow graph across entire scopes. This creates transaction flows, where a TL-X processor is able to pull signals produced early in the flow to a consumption later in the flow. If a search back through a transaction flow fails to find producer(s), the TL-X processor will report an error.
For example,
|in_pipe
@0
$data = ...;
|out_pipe
@3
$ANY = $bypassed ? /top|in_pipe<<2$ANY
: /top|in_pipe<>0$ANY;
@4
... = $data;
the code above pulls |in_pipe$data through the $ANY mux into |out_pipe$data. $data is pulled through the mux from either |in_pipe@1 if $bypassed or |in_pipe@3 otherwise. Other pipe signals consumed in |out_pipe can also be pulled through the $ANY mux.
Essentially, the name spaces of |in_pipe and |out_pipe are unified, though this is not precisely the case, as same-named signals could exist in both scopes as independent assignments. While this is generally bad practice, no error will be generated. The $ANY graph will not be searched if a local signal exists.
Comments use HDL comment syntax. They may appear within assignment statements and blocks or after scope identifiers (together with their range). Scope identifiers must begin their line, after indentation, so it is not legal to have a scope identifier after a multi-line-style comment that is terminated on the same line. Comments are otherwise ignored from the standpoint of indentation. Comments may begin anywhere in their line, including at the line-type character.
TL-X evolved from research and development at Intel Corporation, led by Steve Hoover. (Steve is an active contributor to TL-X and Founder of Redwood EDA.) Yura Pyatnychko also contributed significantly to the work. Countless others made supporting contributions.