ordec.ord — ORD language
ORD is ORDeC’s current programming language. It offers full support of Python, plus additional ORD syntax (a Python-superset) to improve textual IC design within the ORDeC project. It currently focuses on simplifying schematic entry and layout while also supporting regular Python syntax for simulations. Execution of ORD code results in a one-pass compiler step that transforms the input into context-based Python code.
This is only made possible by leveraging the power of
ViewContext and
NodeContext, which are explained in a later
paragraph. The actual ORD grammar is written in Lark. Lark is a well-known and
efficient Python parsing framework for grammars in EBNF form. The function call
ord_to_py() summarizes the necessary function calls for a proper
ORD-to-Python conversion. The conversion is mostly dependent on the
OrdTransformer that inherits from PythonTransformer. The
PythonTransformer is capable of transforming any Python code written in ORD
back to Python, and the OrdTransformer handles the conversion of the ORD
syntax. The following paragraphs summarize the logic behind the
ORD-to-Python conversion.
For a practical demonstration, please visit the ORD tutorial ORD Language Tutorial page!
ORD to Python in Detail
ORD is not a general-purpose programming language. It is developed to simplify certain steps in IC design, especially for the ORDeC project. The entire backend of ORDeC is written in Python, but using Python for tasks like schematic entry can become complicated and cumbersome. ORD represents a more convenient syntax layer that makes structuring and describing IC designs much easier.
Mastering the ORD language requires understanding two crucial parts. First, the ORD language itself: what it offers and what it represents. Second, the converted code: understanding how ORD code is converted back to Python. This helps, especially if you run into trouble while programming or designing, and it also helps you understand how the project works under the hood. Especially for complex programs and debugging purposes, understanding the Python side can become important.
ORD Contexts
The dotted syntax of ORD, which accesses the current context element, requires
having a reference to that element. This structure therefore necessitates that
statements and expressions inside a context block have a reference to the
parent even after transformation of ORD back to Python. This logic is
implemented with ViewContext for the active view
and NodeContext for the currently active node.
They use the Python with environment together with a context variable
ContextVar to always maintain a reference without requiring
information about the parent during transformation. With ORD, we try to keep
the transformation logic as simple as possible and leverage the power of
Python to supply the necessary constructs during execution.
# Type 1
port xyz:
.pos=(1,2)
# Type 2
port xyz(.pos=(1,2))
Node Statements
A node statement is the A B construct that creates and names an element in the current context. There are three types of node statements:
Node class statements — the type is a Node subclass, e.g.,
LayoutRect xNode instance statements — the type is a Cell class or instance, e.g.,
Nmos xNode keyword statements — the type is a built-in keyword, e.g.,
input x,output y,port z
A node statement may have an optional body (indented block after :) for setting attributes:
Nmos pd:
.$l = 400n
Or it can be bodyless:
Nmos pd
To demonstrate how the ORD context works and how the conversion from ORD to Python looks, consider the following example:
ORD code
cell Inv:
viewgen symbol -> Symbol:
inout vdd(.align=North)
inout vss(.align=South)
input a(.align=West)
output y(.align=East)
viewgen schematic -> Schematic:
port vdd(.pos=(2,13); .align=North)
port vss(.pos=(2,1); .align=South)
port y (.pos=(9,7); .align=West)
port a (.pos=(1,7); .align=East)
Nmos pd:
.s -- vss
.b -- vss
.d -- y
.pos = (3,2)
.$l = 400n
Pmos pu:
.s -- vdd
.b -- vdd
.d -- y
.pos = (3,8)
.$l = 400n
for instance in pu, pd:
instance.g -- a
Compiled Python code
Note
The actual compiled code uses __ord_context__ instead of context to
avoid name collisions with user code. Here we use
import ordec.ord.context as context for readability when calling helper
functions such as add and root.
At the start of a view generator, the compiler creates __ord_root__ and
opens the root view context with __ord_root__.view_context(__ord_root__).
Every node statement then saves the created element as a local variable and, if
the statement has a body, opens a nested node context with node.ctx(). The
dotted access is converted into context.root(). Accesses outside the context are still possible
through the local variable. An access like this is visible in the for loop
of the example.
import ordec.ord.context as context
class Inv(Cell):
@generate
def symbol(self) -> Symbol:
__ord_root__ = Symbol(cell=self)
with __ord_root__.view_context(__ord_root__):
vdd = context.add(('vdd',), Pin(pintype=PinType.Inout))
with vdd.ctx():
context.root().align = North
vss = context.add(('vss',), Pin(pintype=PinType.Inout))
with vss.ctx():
context.root().align = South
a = context.add(('a',), Pin(pintype=PinType.In))
with a.ctx():
context.root().align = West
y = context.add(('y',), Pin(pintype=PinType.Out))
with y.ctx():
context.root().align = East
return __ord_root__
@generate
def schematic(self) -> Schematic:
__ord_root__ = Schematic(cell=self, symbol=self.symbol)
with __ord_root__.view_context(__ord_root__):
vss = context.add_port(('vss',))
with vss.ctx():
context.root().pos = (2,1)
context.root().align = South
vdd = context.add_port(('vdd',))
with vdd.ctx():
context.root().pos = (2,13)
context.root().align = North
y = context.add_port(('y',))
with y.ctx():
context.root().pos = (9,7)
context.root().align = West
a = context.add_port(('a',))
with a.ctx():
context.root().pos = (1,7)
context.root().align = East
pd = context.add_element(('pd',), Nmos)
with pd.ctx():
context.root().s -- vss
context.root().b -- vss
context.root().d -- y
context.root().pos = (3,2)
context.root().params.l = R('400n')
pu = context.add_element(('pu',), Pmos)
with pu.ctx():
context.root().s -- vdd
context.root().b -- vdd
context.root().d -- y
context.root().pos = (3,8)
context.root().params.l = R('400n')
for instance in pu, pd:
instance.g -- a
return __ord_root__
Anonymous Node Statements
Prepending a node statement with the anonymous keyword creates the node
without registering it in the ORDB path system. The node is still assigned
to a local Python variable, so it can be referenced in subsequent code. This
is useful inside loops or other situations where multiple nodes of the same type
would cause NPath name clashes:
for sd in (.m8.sd[1], .m7.sd[1]):
anonymous LayoutRect r:
.layer = layers.Metal1
! r.contains(sd.rect)
Without anonymous, writing LayoutRect r twice (across loop iterations)
would attempt to register the path name r twice, causing a conflict. With
anonymous, each iteration creates a fresh node that is only accessible
through the local variable r.
Anonymous node statements support all the same forms as regular node statements:
# Bodyless
anonymous Pin a
# With body
anonymous LayoutRect r:
.layer = layers.Metal1
# Multiple targets (bodyless only)
anonymous Pin x, y, z
anonymous is a soft keyword: it can still be used as a regular
identifier (variable name, function name, etc.) in all other contexts.
Internally, anonymous LayoutRect r compiles to
r = context.add_element(None, LayoutRect). When add receives None
as the name tuple, it adds the node to the subgraph without creating an NPath
entry.
Connection Operator --
The -- operator connects an instance pin to a net (or vice versa). It is
not a dedicated grammar rule but a pseudo-operator that relies on standard
Python parsing: a -- b is parsed as a - (-b), combining subtraction
(__sub__) and negation (__neg__). Both operand orders are supported,
so inst.d -- vss and vss -- inst.d are equivalent.
Internally, the negation step returns a NegatedWireOperand and the
subtraction step detects this sentinel and calls __wire_op__ to create the
actual connection node (SchemInstanceConn or
SchemInstanceUnresolvedConn).
# These two forms are equivalent:
inst.d -- vss # pin -- net
vss -- inst.d # net -- pin
# Python sees: inst.d.__sub__(vss.__neg__())
# or: vss.__sub__(inst.d.__neg__()) → fallback to _NegatedForWire.__rsub__
Because -- is plain Python arithmetic, it coexists with regular numeric
expressions: 2 -- 2 evaluates to 4 as expected.
The following summary shows the most important functions and classes of ORD. Please refer to the Python codebase for more background information and details.
Parser
- ordec.ord.parser.parse_with_errors(parser, code)
Function which parses an ORD string with improved error messages
- Parameters:
parser – ORD Lark parser
code (str) – String containing ORD code
- Returns:
AST of the parsed string
- ordec.ord.parser.ord_to_py(ord_string: str) Module
Function which parses an ORD string and returns the transformed result.
- Parameters:
ord_string (str) – String containing ORD code
- Returns:
AST of the parsed and transformed string
Contexts
- class ordec.core.context.NodeContext(root)
Class which represents the context where a specific ORDB element is alive and accessible via relative accesses (dotted notation)
- class ordec.core.context.ViewContext(root)
Base view context. Subclasses override to add capabilities (e.g. constraint solving).
ViewContext is a separate context from the NodeContext but its __enter__ and __exit__ methods also automatically enter and exit a corresponding NodeContext.
- postprocess()
Override in subclasses to perform finalization on context exit.
- class ordec.core.context.LayoutViewContext(root)
OrdTransformer
- class ordec.ord.ord_transformer.OrdTransformer(source_text='')
Bases:
PythonTransformerThe OrdTransformer handles ORD-specific syntax and converts it back to valid Python ORDeC code. It inherits from the PythonTransformer for full support of the Python syntax.
- celldef(nodes)
Definition of a ORDeC cell class
- RATIONAL(token)
Rational numbers with SI suffix (100n, 20u)
- viewgen(nodes)
Funcdef for cell (viewgen name -> Type: suite)
- constrain_stmt(nodes)
! x >= 200
- extract_path(nodes)
Extract string list from nested attributes
- node_stmt(nodes)
Node statement: ‘Type name’ with optional body.
There are three types of node statements: - Node class statements: e.g., LayoutRect x - Node instance statements: e.g., Nmos x - Node keyword statements: e.g., input x, port x
- anon_node_stmt(nodes)
Anonymous node statement: ‘anonymous Type name’ with optional body.
Like node_stmt but passes None as name_tuple, so no NPath is created.
- anon_node_stmt_nobody(nodes)
Anonymous node statement without body, supports multiple names.
- node_stmt_nobody(nodes)
Node statement without body, supports multiple names (e.g., ‘Nmos a, b, c’)
- dotted_atom(nodes)
Dotted name (.x) or bare dot (.) - access current context root
- getparam(nodes)
get/set param (.$l = 100n)
- net_and_path_stmt_helper(nodes, stmt)
Helper for similar code from net and path statements
- net_stmt(nodes)
Add net (net x)
- path_stmt(nodes)
Add path (path x)
PythonTransformer
- class ordec.ord.python_transformer.PythonTransformer(source_text='')
Transformer that transforms any Python code back into a Python AST. This Class represents the base of the ORD language