Veryl の IR で遊んでみる

- 独自 target を生やしてみたかった話 -


Ryoga.exe

Kernel/VM探検隊@つくば No3

自己紹介

Ryoga.exe

  • 筑波大学情報学群情報科学類 3 年
  • やる:言語処理系とFPGAとCTF
  • X (Twitter)/mixi2: @Ryoga_exe

最近作ったもの

  • FPGA 上に RISC-V CPU を作ったりした
  • その上で動く簡易的な OS も作った
  • その上でテトリスとかが動いた

筑波大学の COJT とかいう謎授業の自由課題で作った(CPU, OS 自作して単位が貰えて最高!)

CPU を作る中で Veryl という
モダンなハードウェア記述言語に出会った

Veryl とは

  • SystemVerilog ベースの新しい HDL
  • モダンで書きやすい構文
  • 既存の SystemVerilog 資産と連携しやすい
  • フォーマッタなどツール込みで使いやすい
Veryl の公式サイト

最近 IR が導入されたらしい

最近 IR が導入されたらしい

  • もともと Veryl は CST ベースの処理が多かった
  • しかし、エラーチェック(未代入やラッチ検出)には IR のほうが向いている

そこで IR が導入された

IR が入ると何が嬉しいのか?

  • 条件分岐が単純な形に落ちる
  • generics / parameter override を反映した状態で見られる
  • ビット単位の未代入検出/ラッチ検出がしやすい

将来的には IR を評価してシミュレータも
見えてくるらしい

思った

独自 target 生やせるのではないか

  • 公式 target は SystemVerilog
  • でも IR が公開されているなら、遊べそう

外から触れるのか

  • Veryl のリポジトリは Cargo workspace
  • crates/ 配下に analyzer などの crate がある
  • veryl-analyzerpub mod ir; を公開していて、AnalyzerContext も public に export している
[dependencies]
veryl-analyzer = "0.19.0"
veryl-metadata = "0.19.0"
veryl-parser = "0.19.0"
use std::{env, fs};
use veryl_analyzer::{Analyzer, Context, ir::Ir};
use veryl_metadata::Metadata;
use veryl_parser::Parser;
fn main() {
let path = env::args().nth(1).unwrap();
let code = fs::read_to_string(&path).unwrap();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(&code, &path).unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
let mut errors = vec![];
errors.append(&mut analyzer.analyze_pass1("prj", &parser.veryl));
errors.append(&mut Analyzer::analyze_post_pass1());
errors.append(&mut analyzer.analyze_pass2("prj", &parser.veryl, &mut context, Some(&mut ir)));
errors.append(&mut Analyzer::analyze_post_pass2());
if !errors.is_empty() {
eprintln!("analyzer errors:");
for e in &errors {
eprintln!("{e:?}");
}
eprintln!();
}
println!("{}", ir);
}
use std::{env, fs};
use veryl_analyzer::{Analyzer, Context, ir::Ir};
use veryl_metadata::Metadata;
use veryl_parser::Parser;
fn main() {
let path = env::args().nth(1).unwrap();
let code = fs::read_to_string(&path).unwrap();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(&code, &path).unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
let mut errors = vec![];
errors.append(&mut analyzer.analyze_pass1("prj", &parser.veryl));
errors.append(&mut Analyzer::analyze_post_pass1());
errors.append(&mut analyzer.analyze_pass2("prj", &parser.veryl, &mut context, Some(&mut ir)));
errors.append(&mut Analyzer::analyze_post_pass2());
if !errors.is_empty() {
eprintln!("analyzer errors:");
for e in &errors {
eprintln!("{e:?}");
}
eprintln!();
}
println!("{}", ir);
}
use std::{env, fs};
use veryl_analyzer::{Analyzer, Context, ir::Ir};
use veryl_metadata::Metadata;
use veryl_parser::Parser;
fn main() {
let path = env::args().nth(1).unwrap();
let code = fs::read_to_string(&path).unwrap();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(&code, &path).unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
let mut errors = vec![];
errors.append(&mut analyzer.analyze_pass1("prj", &parser.veryl));
errors.append(&mut Analyzer::analyze_post_pass1());
errors.append(&mut analyzer.analyze_pass2("prj", &parser.veryl, &mut context, Some(&mut ir)));
errors.append(&mut Analyzer::analyze_post_pass2());
if !errors.is_empty() {
eprintln!("analyzer errors:");
for e in &errors {
eprintln!("{e:?}");
}
eprintln!();
}
println!("{}", ir);
}
use std::{env, fs};
use veryl_analyzer::{Analyzer, Context, ir::Ir};
use veryl_metadata::Metadata;
use veryl_parser::Parser;
fn main() {
let path = env::args().nth(1).unwrap();
let code = fs::read_to_string(&path).unwrap();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(&code, &path).unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
let mut errors = vec![];
errors.append(&mut analyzer.analyze_pass1("prj", &parser.veryl));
errors.append(&mut Analyzer::analyze_post_pass1());
errors.append(&mut analyzer.analyze_pass2("prj", &parser.veryl, &mut context, Some(&mut ir)));
errors.append(&mut Analyzer::analyze_post_pass2());
if !errors.is_empty() {
eprintln!("analyzer errors:");
for e in &errors {
eprintln!("{e:?}");
}
eprintln!();
}
println!("{}", ir);
}
use std::{env, fs};
use veryl_analyzer::{Analyzer, Context, ir::Ir};
use veryl_metadata::Metadata;
use veryl_parser::Parser;
fn main() {
let path = env::args().nth(1).unwrap();
let code = fs::read_to_string(&path).unwrap();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(&code, &path).unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
let mut errors = vec![];
errors.append(&mut analyzer.analyze_pass1("prj", &parser.veryl));
errors.append(&mut Analyzer::analyze_post_pass1());
errors.append(&mut analyzer.analyze_pass2("prj", &parser.veryl, &mut context, Some(&mut ir)));
errors.append(&mut Analyzer::analyze_post_pass2());
if !errors.is_empty() {
eprintln!("analyzer errors:");
for e in &errors {
eprintln!("{e:?}");
}
eprintln!();
}
println!("{}", ir);
}
use std::{env, fs};
use veryl_analyzer::{Analyzer, Context, ir::Ir};
use veryl_metadata::Metadata;
use veryl_parser::Parser;
fn main() {
let path = env::args().nth(1).unwrap();
let code = fs::read_to_string(&path).unwrap();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(&code, &path).unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
let mut errors = vec![];
errors.append(&mut analyzer.analyze_pass1("prj", &parser.veryl));
errors.append(&mut Analyzer::analyze_post_pass1());
errors.append(&mut analyzer.analyze_pass2("prj", &parser.veryl, &mut context, Some(&mut ir)));
errors.append(&mut Analyzer::analyze_post_pass2());
if !errors.is_empty() {
eprintln!("analyzer errors:");
for e in &errors {
eprintln!("{e:?}");
}
eprintln!();
}
println!("{}", ir);
}
use std::{env, fs};
use veryl_analyzer::{Analyzer, Context, ir::Ir};
use veryl_metadata::Metadata;
use veryl_parser::Parser;
fn main() {
let path = env::args().nth(1).unwrap();
let code = fs::read_to_string(&path).unwrap();
let metadata = Metadata::create_default("prj").unwrap();
let parser = Parser::parse(&code, &path).unwrap();
let analyzer = Analyzer::new(&metadata);
let mut context = Context::default();
let mut ir = Ir::default();
let mut errors = vec![];
errors.append(&mut analyzer.analyze_pass1("prj", &parser.veryl));
errors.append(&mut Analyzer::analyze_post_pass1());
errors.append(&mut analyzer.analyze_pass2("prj", &parser.veryl, &mut context, Some(&mut ir)));
errors.append(&mut Analyzer::analyze_post_pass2());
if !errors.is_empty() {
eprintln!("analyzer errors:");
for e in &errors {
eprintln!("{e:?}");
}
eprintln!();
}
println!("{}", ir);
}

input (veryl):

module Counter (
    clk: input clock,
    rst: input reset,
    o  : output logic<8>,
) {
    always_ff {
        if_reset {
            o = 0;
        } else {
            o += 1;
        }
    }
}

output (ir):

module Counter {
  input var0(clk): clock = 1'hx;
  input var1(rst): reset = 1'hx;
  output var2(o): logic<8> = 8'hxx;

  ff (var0, var1) {
    if_reset {
      var2 = 32'sh00000000;
    } else {
      var2 = (var2 + 32'sh00000001);
    }
  }
}

眺めた感じ

どうにか何かはできそう

何を target にするか

SystemVerilog との共通点を考える

何を target にするか

SystemVerilog

  • 仕様書がデカい
  • 組合せ回路が記述できる
  • フリップフロップ回路が記述できる
  • CPU を作るのに使われる
  • 仕様書がデカい
  • 組合せ回路(っぽいもの)が記述できる
  • フリップフロップ回路(っぽいもの)が記述できる
  • CPU を作るのに使われる

Veryl to CSS を試みた話



Ryoga.exe

Kernel/VM探検隊@つくば No3

近年 CSS の進化は目覚ましい

CSS の表現力が劇的に向上

CSS で CPU が作られたことは記憶に新しい

しかし

  • CSS で複雑なロジックを組むのは大変
  • 実は CSS は複雑なロジックを組むのが目的の言語ではない

実は

Cascading Style Sheets

実は

Cascading Style Sheets

実は文書のスタイリングが主目的

なので

複雑なロジック部分をVerylで実装することにより
高度なCSSを実現する

input (veryl):

module Top (
    a: input  i32,
    b: input  i32,
    c: output i32,
) {
    var t0: i32;
    always_comb {
        t0 = a + b;
        c = t0 + 5;
    }
}

output (css):

:root {
  --t0: calc(var(--a) + var(--b));
  --c: calc(var(--t0) - 5);
}

つくったもの

GitHub https://github.com/Ryoga-exe/veryl-css

何ができるか

GitHub https://github.com/Ryoga-exe/veryl-css

  • always_comb / alawys_ff / if_reset / if / case / var
  • 一部の組込み型
  • 一部の演算子

を CSS にコンパイル可能

module seg7 (
    d0 : input  u8,
    d1 : input  u8,
    d2 : input  u8,
    d3 : input  u8,
    out: output u8,
    s0 : output u8,
    s1 : output u8,
    s2 : output u8,
    s3 : output u8,
    s4 : output u8,
    s5 : output u8,
    s6 : output u8,
) {
    var data: u8;
    var seg: u8;
    always_comb {
        data = 8 * d0 + 4 * d1 + 2 * d2 + 1 * d3;
        out = data;
        case data {
            0      : seg = 8'b0111111;
            1      : seg = 8'b0000110;
            2      : seg = 8'b1011011;
            3      : seg = 8'b1001111;
            4      : seg = 8'b1100110;
            5      : seg = 8'b1101101;
            6      : seg = 8'b1111101;
            7      : seg = 8'b0000111;
            8      : seg = 8'b1111111;
            9      : seg = 8'b1101111;
            default: seg = 0;
        }
        s0 = seg[0];
        s1 = seg[1];
        s2 = seg[2];
        s3 = seg[3];
        s4 = seg[4];
        s5 = seg[5];
        s6 = seg[6];
    }
}
/* generated by veryl-css */

@function --veryl-eq(--a <number>, --b <number>) returns <integer> {
  result: clamp(0, calc(1 - abs(sign(calc(var(--a) - var(--b))))), 1);
}

@property --veryl-cond-0 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-1 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-2 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-3 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-4 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-5 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-6 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-7 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-8 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --veryl-cond-9 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --d0 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --d1 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --d2 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --d3 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --out {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --s0 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --s1 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --s2 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --s3 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --s4 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --s5 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --s6 {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --seg7-data {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --seg7-seg {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

:root {
  --seg7-data: calc((calc((calc((calc((8) * (var(--d0)))) +
    (calc((4) * (var(--d1)))))) +
    (calc((2) * (var(--d2)))))) +
    (calc((1) * (var(--d3)))));
  --out: var(--seg7-data);
  --veryl-cond-0: --veryl-eq(var(--seg7-data), 0);
  --veryl-cond-1: --veryl-eq(var(--seg7-data), 1);
  --veryl-cond-2: --veryl-eq(var(--seg7-data), 2);
  --veryl-cond-3: --veryl-eq(var(--seg7-data), 3);
  --veryl-cond-4: --veryl-eq(var(--seg7-data), 4);
  --veryl-cond-5: --veryl-eq(var(--seg7-data), 5);
  --veryl-cond-6: --veryl-eq(var(--seg7-data), 6);
  --veryl-cond-7: --veryl-eq(var(--seg7-data), 7);
  --veryl-cond-8: --veryl-eq(var(--seg7-data), 8);
  --veryl-cond-9: --veryl-eq(var(--seg7-data), 9);
  --seg7-seg: if(style(--veryl-cond-0: 1): 63;
    else: if(style(--veryl-cond-1: 1): 6;
    else: if(style(--veryl-cond-2: 1): 91;
    else: if(style(--veryl-cond-3: 1): 79;
    else: if(style(--veryl-cond-4: 1): 102;
    else: if(style(--veryl-cond-5: 1): 109;
    else: if(style(--veryl-cond-6: 1): 125;
    else: if(style(--veryl-cond-7: 1): 7;
    else: if(style(--veryl-cond-8: 1): 127;
    else: if(style(--veryl-cond-9: 1): 111;
    else: 0))))))))));
  --s0: mod(var(--seg7-seg), 2);
  --s1: mod(round(down, var(--seg7-seg) / 2), 2);
  --s2: mod(round(down, var(--seg7-seg) / 4), 2);
  --s3: mod(round(down, var(--seg7-seg) / 8), 2);
  --s4: mod(round(down, var(--seg7-seg) / 16), 2);
  --s5: mod(round(down, var(--seg7-seg) / 32), 2);
  --s6: mod(round(down, var(--seg7-seg) / 64), 2);
}
module Counter (
    clk: input clock,
    rst: input reset,
    o  : output i32,
) {
    always_ff {
        if_reset {
            o = 0;
        } else {
            o = o + 1;
        }
    }
}
/* generated by veryl-css */

@property --o {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

@property --rst {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

body {
  --o: var(--Counter-o-hoist, 0);
  --Counter-o-next: if(style(--rst: 1): 0; else: calc((var(--o)) + (1)));
}

@keyframes hoist {
  0%, 100% {
    --Counter-o-hoist: var(--Counter-o-captured, 0);
  }
}

@keyframes capture {
  0%, 100% {
    --Counter-o-captured: var(--Counter-o-next);
  }
}

制約

  • 扱える型に制限がある
  • 現状 module は一つだけ
  • 動作環境は新しめの Chromium ブラウザのみ
  • クロックの扱いが厳密ではない

実装

Veryl ソース

↓ veryl-parser + veryl-analyzer

IR(型情報付き変数・文のツリー)

↓ codegen.rs

CSS

本質:emit_module() 関数

  1. clock/reset/ff 割り当て先の ID 収集
  2. 変数ごとに CSS 変数名・型情報・ff 用中間変数を確定
  3. comb/ff を変換
  4. CSS テキストを組み立てて出力

if() の取り扱い:eval_if_expr()

CSS には if() があるが、
単純な等値比較しか扱えない

if(style(--foobar: 1): 10; else: 20;)

↑ 時代を先取りしすぎているため、うまくシンタックスハイライトが効かない

そういえば最近のCSSのアップデートは目覚ましい

ここでおもむろに CSS WG の CSS Values and Units Module Level 4 を見てみる

  • 端数処理: round
  • 剰余関数: rem, mod
  • 冪乗: pow
  • 平方根: sqrt
  • 対数: log
  • 指数: exp
  • 絶対値: abs
  • min / max / clamp
  • 符号関数: sign
  • 三角関数・逆三角関数: sin, cos, tan, asin, ...
  • ネイピア数 ($e$) や円周率 ($\pi$) などの定数
/* a < b なら 1、それ以外は 0 */
@function --lt(--a <number>, --b <number>) returns <integer> {
  result: clamp(0, sign(calc(var(--b) - var(--a))), 1);
}

/* a > b なら 1、それ以外は 0 */
@function --gt(--a <number>, --b <number>) returns <integer> {
  result: clamp(0, sign(calc(var(--a) - var(--b))), 1);
}

→ これに入れて 1 かどうかを比較すればよい

まとめ

GitHub https://github.com/Ryoga-exe/veryl-css

  • かなり無理矢理 CSS ターゲットを実装した
  • Veryl の IR で、ある程度遊ぶことができる
  • CSS は多分 HDL ではないが、可能性はある
  • CSS でヘンなことをするなら仕様書を当たると良いことがある