A full rewrite of the Lucid parser in Alchitry Labs has been a long time coming.

The current parser has had feature after feature crudely bolted on to a shaky foundation. It was originally only written to convert Lucid into Verilog. Later, error checking for signal widths was added. Then full parsing on constant values was added. Then parsing of non-constant values was kind of added. Then... then... then...

A lot of the code is confusing and many sections are redundant. On top of that, null checks weren't my specialty and I often forgot to check when something could've been null resulting in a crash when bad syntax was entered.

The new parser is fixing all this while adding many more features. It is written in Kotlin which largely fixes null checks by having null safe types built into the language.

Breaking Changes

I'm also taking this opportunity to re-imagine some aspects of Lucid. Because of this, Lucid 2 will break compatibility with the original Lucid.

For example, currently you can use the .WIDTH property of signals in Lucid to access their dimensions (number of bits). In Lucid 2, this is getting replaced by the $widthOf() function. This will allow you to not only get the widths of signals but also expressions. It also helps clean up the internal code when resolving signals.

The fsm type is being removed and being replaced with enum. The fsm type was kind of a strange type in that it really was just a dff with an associated set of constants. The new method is to use enum to declare a set of constants and use a dff to hold them. Here's an example.

enum myFSM { INIT, START, RUN, STOP }
dff myDff[$widthOf(myFSM)]

always {
    myDff.d = myFSM.START
}

This simplifies things by making dff the only sequential type.

The eagle eyed among you may have noticed that there aren't any semicolons in the above code snippet. Semicolons are optional in Lucid 2.

The var type is also being removed. The only real use for the var type was in for loops which you could still use a sig type for. It wasn't particularly intuitive as it had the size of a computer Int when everything else a single bit.

Speaking of for loops, these are replaced with repeat blocks. These have the syntax repeat(count, var) { ... } where count is the number of times to repeat the block and var is the name to use for the current iteration index. This signal is automatically generated for you and you don't need to declare it somewhere else. It is only visible inside the repeat block.

There are a handful of other minor changes. When declaring a dff or sig, you can now only declare one per line. Declaring multiple off the same keyword was rarely used and often made the code harder to read. Removing this also made the backend code cleaner.

The struct portion of a declaration was moved after the array indices. An old dff declaration with that used a struct used to look like this dff<myStruct> myDff[8] (.clk(clk));. It now looks like this dff myDff[8]<myStruct> (.clk(clk)). The original style was based on generics in languages like Java, but the new style fits better with how you actually index the values. It makes it much clearer that myDff is an array of structs.

New Additions

There will be some new functions to help when working with fixed point numbers. I haven't nailed all these down yet but something along the lines of $fixedPoint(3.14159, 8, 4) will generate an 8-bit wide number with 4 bits used for the decimal (aka 8b00110010) that is the closest approximation to the 3.14159. There will also be ceiling and floor versions that generate the closet value above or below the given value respectively.

I'm also planning to implement interfaces. These will basically be like struct but each member will have a direction associated with it. I found myself often creating two struct where one would be for inputs and one would be for outputs. For example, the memory.in and memory.out structs used by the DDR interface.

Interfaces will allow combining these into a single port. There will be "a" and "b" versions of each interface where the "b" version is a mirrored copy of "a" (inputs are outputs, outputs are inputs). That way a module with an "a" port can directly connect to a module with a "b" port.

Simulation

A big motivator for the rewrite was the potential to do full Lucid simulations. The old code would parse constant expressions, but it only hinted at the possibility to do a full simulation by iterating the parsing.

The new code builds a full model of your project complete with signal connections, models for always blocks, and other dynamic expressions. This not only helps with more robust error checking (it's hard to miss something when you're required to actually model it all) but will allow for quick simple simulations to check if your logic is working how you expect.

Much of this is already working. The details are more than the scope of this post, but check out the link to the source below for details on how it works.

As part of doing simulations, a new testBench will be created. I haven't nailed down the specifics for this either just quiet yet but the current idea is for a testBench to look similar to a module. Inside the testBench, you will be able to instantiate your module to test. You can then test it via test and function blocks.

The test blocks will work similar to an always block in that the contents will run sequentially.

The function blocks will be the same except they won't run on their own and can instead be called by test blocks. I'm imagining these being used for common tasks like cycling a clock. For example, something like repeat(100) { cycleClock() }. The syntax for this is very much still up in the air.

Source and Discussion

Head over to our GitHub page to check out the current state of things.

There is also a discussion page setup as part of the repo where you can let me know your thoughts.