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:

  1. Node class statements — the type is a Node subclass, e.g., LayoutRect x

  2. Node instance statements — the type is a Cell class or instance, e.g., Nmos x

  3. Node 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: PythonTransformer

The 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