diff --git a/.clangd b/.clangd new file mode 100644 index 000000000..428885c79 --- /dev/null +++ b/.clangd @@ -0,0 +1,23 @@ +If: + PathMatch: crates/slang-sys/src/.*\.(cc|cpp|h|hpp)$ + +CompileFlags: + CompilationDatabase: None + Add: + - -std=c++20 + - -I. + - -I../../../third_party/slang/include + - -I../../../third_party/slang/external + - -I../../../target/slang-sys/debug/include + - -I../../../target/slang-sys/cxx/include + - -include + - cstdlib + - -DSLANG_BOOST_SINGLE_HEADER + - -DSLANG_STATIC_DEFINE + +--- +If: + PathMatch: third_party/slang/.* + +CompileFlags: + CompilationDatabase: target/slang-sys/debug/build diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index ee37dfc20..ea1673ce9 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -2,10 +2,6 @@ name: Setup pinned Rust description: Install the Rust toolchain pinned by rust-toolchain.toml. inputs: - components: - description: Comma-separated Rust components to install. - required: false - default: "" targets: description: Comma-separated Rust targets to install. required: false @@ -19,21 +15,28 @@ outputs: runs: using: composite steps: - - name: Read pinned toolchain + - name: Install pinned Rust + shell: bash + run: rustup toolchain install --no-self-update + + - name: Install Rust targets + if: inputs.targets != '' + shell: bash + env: + RUST_TARGETS: ${{ inputs.targets }} + run: | + set -euo pipefail + IFS=',' read -ra targets <<< "${RUST_TARGETS}" + rustup target add "${targets[@]}" + + - name: Resolve active toolchain id: toolchain shell: bash run: | set -euo pipefail - toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' "${GITHUB_WORKSPACE}/rust-toolchain.toml")" + toolchain="$(rustup show active-toolchain | awk '{ print $1 }')" if [ -z "${toolchain}" ]; then - echo "rust-toolchain.toml does not define a toolchain channel." >&2 + echo "rustup did not report an active toolchain." >&2 exit 1 fi echo "toolchain=${toolchain}" >> "${GITHUB_OUTPUT}" - - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ steps.toolchain.outputs.toolchain }} - components: ${{ inputs.components }} - targets: ${{ inputs.targets }} diff --git a/.github/workflows/build-artifacts.yml b/.github/workflows/build-artifacts.yml index 6909427cc..dc0ea5ebf 100644 --- a/.github/workflows/build-artifacts.yml +++ b/.github/workflows/build-artifacts.yml @@ -65,6 +65,8 @@ jobs: CARGO_PROFILE_RELEASE_INCREMENTAL: "false" steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Rust uses: ./.github/actions/setup-rust @@ -143,6 +145,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: recursive - name: Download binary artifacts uses: actions/download-artifact@v4 @@ -182,6 +185,8 @@ jobs: working-directory: editors/vscode steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore playground WASM cache id: playground-wasm-cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c8664f29..c4e3a8698 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: plan: ${{ steps.plan.outputs.result }} steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Detect changed paths id: filter if: github.event_name != 'workflow_dispatch' @@ -67,10 +69,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install Rust uses: ./.github/actions/setup-rust - with: - components: clippy, rustfmt - name: Setup sccache uses: ./.github/actions/setup-sccache with: @@ -99,6 +101,8 @@ jobs: matrix: ${{ fromJSON(needs.changes.outputs.plan).rust_test_matrix }} steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install Rust uses: ./.github/actions/setup-rust - name: Setup sccache @@ -126,6 +130,8 @@ jobs: working-directory: editors/vscode steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Node uses: actions/setup-node@v4 with: @@ -147,6 +153,8 @@ jobs: working-directory: editors/vscode steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore playground WASM cache id: playground-wasm-cache uses: actions/cache/restore@v4 @@ -221,4 +229,3 @@ jobs: build-kind: nightly cargo-profile: debug vsix-profile: debug - diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 91c2e003b..6ec447a16 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -327,6 +327,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_sha || github.event.release.tag_name || github.ref }} + submodules: recursive - name: Restore playground WASM cache id: playground-wasm-cache diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 568b9c361..8b503dc9a 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -75,6 +75,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore playground WASM cache id: playground-wasm-cache diff --git a/.github/workflows/publish-vsix.yml b/.github/workflows/publish-vsix.yml index b03608f77..8153acca9 100644 --- a/.github/workflows/publish-vsix.yml +++ b/.github/workflows/publish-vsix.yml @@ -50,6 +50,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup Node uses: actions/setup-node@v4 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..37007436b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/slang"] + path = third_party/slang + url = https://github.com/MikePopoloski/slang.git diff --git a/crates/slang-sys/Cargo.toml b/crates/slang-sys/Cargo.toml new file mode 100644 index 000000000..773909bfb --- /dev/null +++ b/crates/slang-sys/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "slang-sys" +description = "Thin Rust bindings for the upstream slang SystemVerilog frontend" +version = "0.1.0" +edition = "2024" +license = "MIT" +build = "build.rs" + +[dependencies] +cxx = { version = "1.0.124", features = ["c++20"] } + +[build-dependencies] +cmake = "0.1.50" +cxx-build = "1.0.124" diff --git a/crates/slang-sys/build.rs b/crates/slang-sys/build.rs new file mode 100644 index 000000000..0406d384f --- /dev/null +++ b/crates/slang-sys/build.rs @@ -0,0 +1,265 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, + process::Command, +}; + +const SLANG_SOURCE_DIR: &str = "../../third_party/slang"; +const SLANG_SYS_SOURCE_DIR: &str = "./src"; +const SCRIPTS_DIR: &str = "./scripts"; +/// Build directory from cargo target directory. +/// This will influence clangd's include path. +const BUILD_DIR: &str = "slang-sys"; + +fn main() { + // Prepare environment + let slang_dir = env_detection::find_slang_dir(); + let source_dir = PathBuf::from(SLANG_SYS_SOURCE_DIR); + let scripts_dir = PathBuf::from(SCRIPTS_DIR); + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is not set")); + let debug = cfg!(debug_assertions); + + // Build + generate_rust_defs(&slang_dir, &out_dir, &scripts_dir); + let install_dir = build_slang(&slang_dir, debug); + build_cxx_bridge(&slang_dir, &install_dir); + + // Setup cargo configuration + setup_linking(&install_dir, debug); + setup_rerun_triggers(&slang_dir, &source_dir, &scripts_dir); +} + +mod env_detection { + use std::{env, path::PathBuf}; + + use super::{BUILD_DIR, SLANG_SOURCE_DIR}; + + pub fn find_slang_dir() -> PathBuf { + let slang_source_dir = PathBuf::from(SLANG_SOURCE_DIR); + if !slang_source_dir.join("CMakeLists.txt").is_file() { + panic!( + "SLANG_SOURCE_DIR is set to {}, but that directory does not contain CMakeLists.txt!\nYou may need to run \"git submodule update --init\" to initialize the submodule", + slang_source_dir.display() + ); + }; + slang_source_dir + } + + pub fn target_linker_flags() -> Option { + env::var("TARGET_LDFLAGS").ok().filter(|flags| !flags.trim().is_empty()) + } + + pub fn target_is_msvc() -> bool { + env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") + } + + pub fn target_is_windows() -> bool { + env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") + } + + pub fn build_dir() -> PathBuf { + let workspace_target_dir = + env::var_os("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|| { + PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set"), + ) + .join("../..") + .join("target") + }); + workspace_target_dir.join(BUILD_DIR) + } + + pub fn find_python() -> PathBuf { + env::var_os("PYTHON").map(PathBuf::from).unwrap_or_else(|| "python3".into()) + } + + pub fn cargo_manifest_dir() -> PathBuf { + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set")) + } +} + +fn generate_rust_defs(slang_dir: &Path, out_dir: &Path, scripts_dir: &Path) { + let python = env_detection::find_python(); + let slang_scripts_dir = slang_dir.join("scripts"); + let generators = [ + ( + scripts_dir.join("generate_syntax_kind.py"), + slang_scripts_dir.join("syntax.txt"), + out_dir.join("syntax_kind.rs"), + Vec::::new(), + ), + ( + scripts_dir.join("generate_token_kind.py"), + slang_scripts_dir.join("tokenkinds.txt"), + out_dir.join("token_kind.rs"), + Vec::::new(), + ), + ( + scripts_dir.join("generate_trivia_kind.py"), + slang_scripts_dir.join("triviakinds.txt"), + out_dir.join("trivia_kind.rs"), + Vec::::new(), + ), + ( + scripts_dir.join("generate_diagnostic.py"), + slang_scripts_dir.join("diagnostics.txt"), + out_dir.join("diagnostic.rs"), + vec![ + "--diagnostics-header".into(), + slang_dir.join("include/slang/diagnostics/Diagnostics.h"), + ], + ), + ]; + + let mut children = generators.map(|(generator, input, output, extra_args)| { + let mut command = Command::new(&python); + command.arg(generator).arg("--input").arg(input); + for arg in extra_args { + command.arg(arg); + } + command + .arg("--out") + .arg(output) + .spawn() + .expect("failed to run slang-sys Rust definition generator") + }); + + for child in &mut children { + let status = child.wait().expect("failed to wait for slang-sys Rust definition generator"); + if !status.success() { + panic!("slang-sys Rust definition generator failed with status {status}"); + } + } +} + +fn build_slang(slang_dir: &Path, debug: bool) -> PathBuf { + let cmake_profile = if debug { "Debug" } else { "Release" }; + let cmake_out_dir = env_detection::build_dir().join(cmake_profile.to_ascii_lowercase()); + let emscripten = env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("emscripten"); + + // Configure CMake build + let config = &mut cmake::Config::new(slang_dir); + config + .out_dir(cmake_out_dir) + .define("FETCHCONTENT_TRY_FIND_PACKAGE_MODE", "NEVER") + .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") + .profile(cmake_profile) + .define("CMAKE_VERBOSE_MAKEFILE", "ON"); + if let Ok(jobs) = env::var("NUM_JOBS") { + config.env("CMAKE_BUILD_PARALLEL_LEVEL", jobs); + } + // Build flags + config + .define("SLANG_MASTER_PROJECT", "OFF") + .define("SLANG_INCLUDE_TESTS", "OFF") + .define("SLANG_INCLUDE_TOOLS", "OFF") + .define("SLANG_INCLUDE_INSTALL", "ON") + .define("SLANG_INCLUDE_PYLIB", "OFF") + // TODO: We may need to support mimalloc in the future. But we need to figure out the + // linking issue first. The default build option of slang will generate mimalloc object file + // rather thant the static library :(. + .define("SLANG_USE_MIMALLOC", "OFF") + // .define("SLANG_RUST_CXXBRIDGE_DIR", cxxbridge_dir.to_string_lossy().as_ref()) + .define("CMAKE_INSTALL_LIBDIR", "lib"); + + if emscripten { + config + .define("CMAKE_TRY_COMPILE_TARGET_TYPE", "STATIC_LIBRARY") + .define("CMAKE_CXX_FLAGS", "-fwasm-exceptions -include cstdlib") + .define("CMAKE_CXX_FLAGS_RELEASE", "-O2 -DNDEBUG") + .define("CMAKE_C_FLAGS_RELEASE", "-O2 -DNDEBUG"); + if let Ok(toolchain_file) = env::var("EMSCRIPTEN_CMAKE_TOOLCHAIN_FILE") { + config.define("CMAKE_TOOLCHAIN_FILE", toolchain_file); + } + } else { + if env_detection::target_is_msvc() { + config.define("CMAKE_MSVC_RUNTIME_LIBRARY", "MultiThreadedDLL"); + } else { + config.cxxflag("-include").cxxflag("cstdlib"); + } + } + + // TODO: Port cmake sccache launcher + + if let Some(linker_flags) = env_detection::target_linker_flags() { + config + .define("CMAKE_EXE_LINKER_FLAGS", linker_flags.as_str()) + .define("CMAKE_SHARED_LINKER_FLAGS", linker_flags.as_str()) + .define("CMAKE_MODULE_LINKER_FLAGS", linker_flags.as_str()); + } + + if !emscripten && !debug && env_detection::target_is_msvc() { + // cmake-rs still sets config-specific MSVC flags for Visual Studio + // generators to preserve /MD or /MT. That replaces CMake's built-in + // Release defaults, while cmake-rs has already filtered optimization + // args out of Cargo's compiler flags. Restore the optimized Release + // settings explicitly until cmake-rs can rely on + // CMAKE_MSVC_RUNTIME_LIBRARY for this path. + config + .define("CMAKE_C_FLAGS_RELEASE", "/O2 /Ob2 /DNDEBUG") + .define("CMAKE_CXX_FLAGS_RELEASE", "/O2 /Ob2 /DNDEBUG"); + } + + config.build() +} + +fn build_cxx_bridge(slang_dir: &Path, install_dir: &Path) { + // Setup clangd include directory for cxx crate + let cxx_header = PathBuf::from( + env::var_os("DEP_CXXBRIDGE1_HEADER") + .expect("DEP_CXXBRIDGE1_HEADER is not set; the cxx crate should expose its C++ header"), + ); + let cxx_include_dir = cxx_header + .parent() + .expect("DEP_CXXBRIDGE1_HEADER should point to a header under an include directory") + .to_path_buf(); + let clangd_include_dir = env_detection::build_dir().join("cxx").join("include"); + fs::create_dir_all(&clangd_include_dir).expect("failed to create clangd cxx include directory"); + fs::copy(cxx_header, clangd_include_dir.join("cxx.h")) + .expect("failed to copy cxx.h for clangd"); + // Build cxx bridge + let mut build = cxx_build::bridge("src/ffi.rs"); + build + .file("src/wrapper.cpp") + .include("src") + .include(cxx_include_dir) + .include(install_dir.join("include")) + .include(slang_dir.join("external")) + .define("SLANG_BOOST_SINGLE_HEADER", None) + .define("SLANG_STATIC_DEFINE", None); + if env_detection::target_is_msvc() { + build.flag_if_supported("/std:c++20"); + } else { + build.flag_if_supported("-std=c++20"); + } + build.compile("slang_sys_bridge"); +} + +fn setup_linking(install_dir: &Path, debug: bool) { + let lib_dir = install_dir.join("lib"); + let emscripten = env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("emscripten"); + let fmt_lib = if debug { "fmtd" } else { "fmt" }; + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + println!("cargo:rustc-link-lib=static:-bundle=svlang"); + println!("cargo:rustc-link-lib=static:-bundle={}", fmt_lib); + if !emscripten && env_detection::target_is_windows() { + // mimalloc's Windows large-page support pulls in these token APIs. + println!("cargo:rustc-link-lib=dylib=Advapi32"); + } +} + +fn setup_rerun_triggers(slang_dir: &Path, source_dir: &Path, scripts_dir: &Path) { + let watch = [ + env_detection::cargo_manifest_dir().join("build.rs").to_string_lossy().to_string(), + slang_dir.to_string_lossy().to_string(), + source_dir.join("ffi.rs").to_string_lossy().to_string(), + source_dir.join("wrapper.cpp").to_string_lossy().to_string(), + source_dir.join("wrapper.h").to_string_lossy().to_string(), + scripts_dir.to_string_lossy().to_string(), + ]; + + for path in watch { + println!("cargo:rerun-if-changed={}", path); + } +} diff --git a/crates/slang-sys/scripts/generate_diagnostic.py b/crates/slang-sys/scripts/generate_diagnostic.py new file mode 100644 index 000000000..968aea73b --- /dev/null +++ b/crates/slang-sys/scripts/generate_diagnostic.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 + +import argparse +import json +import re +import shlex +from pathlib import Path + + +def screaming_snake(name: str) -> str: + parts = re.findall(r"[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+", name) + return "_".join(part.upper() for part in parts) + + +def rust_str(value: str) -> str: + return json.dumps(value) + + +def parse_subsystems(path: Path) -> list[str]: + text = path.read_text() + match = re.search( + r"#define DS\(x\) \\\n(?P.*?)\nSLANG_ENUM_SIZED\(DiagSubsystem", + text, + re.S, + ) + if not match: + raise RuntimeError(f"failed to find DiagSubsystem definition in {path}") + + subsystems = [] + for line in match.group("body").splitlines(): + line = line.strip().rstrip("\\").strip() + if not line: + continue + + item = re.fullmatch(r"x\((?P[A-Za-z_][A-Za-z0-9_]*)\)", line) + if not item: + raise RuntimeError(f"invalid DiagSubsystem entry in {path}: {line}") + subsystems.append(item.group("name")) + + return subsystems + + +def parse_diagnostics(path: Path): + diagnostics_by_subsystem = {} + diagnostic_names = set() + option_map = {} + groups = [] + current_subsystem = "General" + current_group = None + + def finish_group(parts): + nonlocal current_group + if current_group is None: + return + + for part in parts: + if part == "}": + groups.append(current_group) + current_group = None + return + current_group[1].append(part) + + for raw_line in path.read_text().splitlines(): + line = raw_line.strip() + if not line or line.startswith("//"): + continue + + parts = shlex.split(line) + if current_group is not None: + finish_group(parts) + continue + + if parts[0] == "subsystem": + current_subsystem = parts[1] + if current_subsystem not in diagnostics_by_subsystem: + diagnostics_by_subsystem[current_subsystem] = [] + continue + + if parts[0] == "group": + current_group = (parts[1], []) + if parts[2] != "=" or parts[3] != "{": + raise RuntimeError(f"invalid group declaration: {line}") + finish_group(parts[4:]) + continue + + severity_token = parts[0] + if severity_token == "warning": + option_name = parts[1] + name = parts[2] + message = parts[3] + severity = "Warning" + elif severity_token in ("error", "fatal", "note"): + option_name = "" + name = parts[1] + message = parts[2] + severity = severity_token.capitalize() + else: + raise RuntimeError(f"invalid diagnostic entry: {line}") + + if name in diagnostic_names: + raise RuntimeError(f"duplicate diagnostic name: {name}") + diagnostic_names.add(name) + + diagnostic = { + "name": name, + "subsystem": current_subsystem, + "severity": severity, + "message": message, + "option_name": option_name, + } + diagnostics_by_subsystem[current_subsystem].append(diagnostic) + if option_name: + option_map.setdefault(option_name, []).append(name) + + diagnostics = [] + for subsystem in sorted(diagnostics_by_subsystem): + subsystem_diagnostics = sorted( + diagnostics_by_subsystem[subsystem], + key=lambda d: (d["severity"], d["name"], d["message"], d["option_name"]), + ) + for code, diagnostic in enumerate(subsystem_diagnostics): + diagnostic["code"] = code + diagnostics.append(diagnostic) + + if current_group is not None: + raise RuntimeError(f"unterminated diagnostic group: {current_group[0]}") + + return diagnostics_by_subsystem, diagnostics, option_map, groups + + +def render_subsystem(subsystems: list[str]) -> str: + variant_defs = [f" {name} = {index}," for index, name in enumerate(subsystems)] + debug_arms = [f' Self::{name} => "{name}",' for name in subsystems] + from_raw_arms = [ + f" {index} => Some(Self::{name})," + for index, name in enumerate(subsystems) + ] + + return f"""#[repr(u16)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub enum DiagSubsystem {{ +{chr(10).join(variant_defs)} +}} + +impl DiagSubsystem {{ + #[inline] + pub const fn from_raw(raw: u16) -> Option {{ + match raw {{ +{chr(10).join(from_raw_arms)} + _ => None, + }} + }} + + #[inline] + pub const fn as_raw(self) -> u16 {{ + self as u16 + }} +}} + +impl fmt::Debug for DiagSubsystem {{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ + let name = match *self {{ +{chr(10).join(debug_arms)} + }}; + f.write_str(name) + }} +}} +""" + + +def render_severity() -> str: + variants = ["Ignored", "Note", "Warning", "Error", "Fatal"] + variant_defs = [f" {name} = {index}," for index, name in enumerate(variants)] + debug_arms = [f' Self::{name} => "{name}",' for name in variants] + from_raw_arms = [ + f" {index} => Some(Self::{name})," + for index, name in enumerate(variants) + ] + + return f"""#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub enum DiagnosticSeverity {{ +{chr(10).join(variant_defs)} +}} + +impl DiagnosticSeverity {{ + #[inline] + pub const fn from_raw(raw: u8) -> Option {{ + match raw {{ +{chr(10).join(from_raw_arms)} + _ => None, + }} + }} + + #[inline] + pub const fn as_raw(self) -> u8 {{ + self as u8 + }} +}} + +impl fmt::Debug for DiagnosticSeverity {{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ + let name = match *self {{ +{chr(10).join(debug_arms)} + }}; + f.write_str(name) + }} +}} +""" + + +def render_diag_code(subsystems: list[str], diagnostics: list[dict]) -> str: + subsystem_ids = {name: index for index, name in enumerate(subsystems)} + constants = [] + debug_arms = [] + all_values = [] + + for diagnostic in diagnostics: + const_name = screaming_snake(diagnostic["name"]) + subsystem = diagnostic["subsystem"] + constants.append( + f" pub const {const_name}: Self = Self {{ subsystem: {subsystem_ids[subsystem]}, code: {diagnostic['code']} }};" + ) + debug_arms.append(f' Self::{const_name} => "{diagnostic["name"]}",') + all_values.append(f"Self::{const_name}") + + return f"""#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct DiagCode {{ + subsystem: u16, + code: u16, +}} + +impl DiagCode {{ +{chr(10).join(constants)} + + pub const ALL: &'static [Self] = &[{", ".join(all_values)}]; + + #[inline] + pub const fn from_raw(subsystem: u16, code: u16) -> Self {{ + Self {{ subsystem, code }} + }} + + #[inline] + pub const fn subsystem_raw(self) -> u16 {{ + self.subsystem + }} + + #[inline] + pub const fn code_raw(self) -> u16 {{ + self.code + }} + + #[inline] + pub const fn subsystem(self) -> Option {{ + DiagSubsystem::from_raw(self.subsystem) + }} + + pub fn info(self) -> Option<&'static DiagnosticInfo> {{ + DIAGNOSTIC_INFOS.iter().find(|info| info.code == self) + }} +}} + +impl fmt::Debug for DiagCode {{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ + let name = match *self {{ +{chr(10).join(debug_arms)} + _ => return write!(f, "DiagCode({{}}, {{}})", self.subsystem, self.code), + }}; + f.write_str(name) + }} +}} +""" + + +def render_infos(diagnostics: list[dict]) -> str: + rows = [] + for diagnostic in diagnostics: + const_name = screaming_snake(diagnostic["name"]) + subsystem = diagnostic["subsystem"] + option = ( + f"Some({rust_str(diagnostic['option_name'])})" + if diagnostic["option_name"] + else "None" + ) + rows.append( + " DiagnosticInfo { " + f"code: DiagCode::{const_name}, " + f"name: {rust_str(diagnostic['name'])}, " + f"subsystem: DiagSubsystem::{subsystem}, " + f"severity: DiagnosticSeverity::{diagnostic['severity']}, " + f"default_message: {rust_str(diagnostic['message'])}, " + f"option_name: {option} " + "}," + ) + + return f"""#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DiagnosticInfo {{ + pub code: DiagCode, + pub name: &'static str, + pub subsystem: DiagSubsystem, + pub severity: DiagnosticSeverity, + pub default_message: &'static str, + pub option_name: Option<&'static str>, +}} + +pub const DIAGNOSTIC_INFOS: &[DiagnosticInfo] = &[ +{chr(10).join(rows)} +]; +""" + + +def render_groups( + groups: list[tuple[str, list[str]]], option_map: dict[str, list[str]] +) -> str: + group_const_defs = [] + group_infos = [] + + for name, options in sorted(groups): + diag_names = [] + for option in sorted(options): + diag_names.extend(option_map[option]) + + const_name = f"{screaming_snake(name)}_GROUP_DIAGNOSTICS" + values = ", ".join( + f"DiagCode::{screaming_snake(diag_name)}" + for diag_name in sorted(diag_names) + ) + group_const_defs.append(f"const {const_name}: &[DiagCode] = &[{values}];") + group_infos.append( + f" DiagnosticGroup {{ name: {rust_str(name)}, diagnostics: {const_name} }}," + ) + + return f"""#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DiagnosticGroup {{ + pub name: &'static str, + pub diagnostics: &'static [DiagCode], +}} + +{chr(10).join(group_const_defs)} + +pub const DIAGNOSTIC_GROUPS: &[DiagnosticGroup] = &[ +{chr(10).join(group_infos)} +]; +""" + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Generate Rust diagnostic definitions from slang diagnostics.txt" + ) + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--diagnostics-header", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + + subsystems = parse_subsystems(args.diagnostics_header) + diagnostics_by_subsystem, diagnostics, option_map, groups = parse_diagnostics( + args.input + ) + missing_subsystems = sorted(set(diagnostics_by_subsystem) - set(subsystems)) + if missing_subsystems: + raise RuntimeError( + f"diagnostics.txt uses unknown subsystem(s): {missing_subsystems}" + ) + + output = "\n\n".join([ + "// This file is generated by crates/slang-sys/scripts/generate_diagnostic.py.", + "// Do not edit by hand.", + "", + "use std::fmt;", + render_subsystem(subsystems), + render_severity(), + render_diag_code(subsystems, diagnostics), + render_infos(diagnostics), + render_groups(groups, option_map), + "", + ]) + + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(output) + + +if __name__ == "__main__": + main() diff --git a/crates/slang-sys/scripts/generate_syntax_kind.py b/crates/slang-sys/scripts/generate_syntax_kind.py new file mode 100644 index 000000000..85518e1eb --- /dev/null +++ b/crates/slang-sys/scripts/generate_syntax_kind.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 + +import argparse +import re +from pathlib import Path + + +def screaming_snake(name: str) -> str: + parts = re.findall(r"[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+", name) + return "_".join(part.upper() for part in parts) + + +def load_syntax_kind_names(path: Path) -> list[str]: + kind_map = {} + current_type = None + current_kind_base = None + current_tags = [] + + def finish_type(): + nonlocal current_type, current_kind_base, current_tags + if current_type is None: + return + + tags = dict(tag.split("=", 1) for tag in current_tags) + is_final = tags.get("final", "true") != "false" + is_multi_kind = tags.get("multiKind") == "true" + if is_final and not is_multi_kind: + if current_type in kind_map: + raise RuntimeError(f"duplicate syntax kind: {current_type}") + kind_map[current_type] = current_type + "Syntax" + + current_type = None + current_kind_base = None + current_tags = [] + + for raw_line in path.read_text().splitlines(): + line = raw_line.strip() + if line.startswith("//"): + continue + + if not line or line == "empty": + current_kind_base = None + finish_type() + continue + + if current_kind_base is not None: + for kind in line.split(): + if kind in kind_map: + raise RuntimeError(f"duplicate syntax kind: {kind}") + kind_map[kind] = current_kind_base + "Syntax" + continue + + if current_type is not None: + continue + + if line.startswith("kindmap<"): + current_kind_base = line[len("kindmap<") : line.index(">")] + continue + + parts = line.split() + current_type = parts[0] + current_tags = parts[1:] + + finish_type() + + return ["Unknown", *sorted(kind_map.keys())] + + +def render_syntax_kind(kinds: list[str]) -> str: + constants = [] + debug_arms = [] + all_values = [] + + for index, kind in enumerate(kinds): + const_name = screaming_snake(kind) + constants.append(f" pub const {const_name}: Self = Self({index});") + debug_arms.append(f' Self::{const_name} => "{kind}",') + all_values.append(f"Self::{const_name}") + + return f"""#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct SyntaxKind(u16); + +impl SyntaxKind {{ +{chr(10).join(constants)} + + pub const ALL: &'static [Self] = &[{", ".join(all_values)}]; + + #[inline] + pub const fn from_raw(raw: u16) -> Self {{ + Self(raw) + }} + + #[inline] + pub const fn as_raw(self) -> u16 {{ + self.0 + }} + + #[inline] + pub const fn is_unknown(self) -> bool {{ + self.0 == Self::UNKNOWN.0 + }} +}} + +impl fmt::Debug for SyntaxKind {{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ + let name = match *self {{ +{chr(10).join(debug_arms)} + _ => return write!(f, "SyntaxKind({{}})", self.0), + }}; + f.write_str(name) + }} +}} +""" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate Rust SyntaxKind from slang syntax.txt") + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + + output = "\n".join( + [ + "// This file is generated by crates/slang-sys/scripts/generate_syntax_kind.py.", + "// Do not edit by hand.", + "", + "use std::fmt;", + "", + render_syntax_kind(load_syntax_kind_names(args.input)), + ] + ) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(output) + + +if __name__ == "__main__": + main() diff --git a/crates/slang-sys/scripts/generate_token_kind.py b/crates/slang-sys/scripts/generate_token_kind.py new file mode 100644 index 000000000..bb6041086 --- /dev/null +++ b/crates/slang-sys/scripts/generate_token_kind.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import argparse +import re +from pathlib import Path + + +def screaming_snake(name: str) -> str: + parts = re.findall(r"[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+", name) + return "_".join(part.upper() for part in parts) + + +def load_token_kind_names(path: Path) -> list[str]: + kinds = [] + for raw_line in path.read_text().splitlines(): + line = raw_line.strip() + if not line or line.startswith("//"): + continue + kinds.append(line) + return kinds + + +def render_token_kind(kinds: list[str]) -> str: + constants = [] + debug_arms = [] + all_values = [] + + for index, kind in enumerate(kinds): + const_name = screaming_snake(kind) + constants.append(f" pub const {const_name}: Self = Self({index});") + debug_arms.append(f' Self::{const_name} => "{kind}",') + all_values.append(f"Self::{const_name}") + + return f"""#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct TokenKind(u16); + +impl TokenKind {{ +{chr(10).join(constants)} + + pub const ALL: &'static [Self] = &[{", ".join(all_values)}]; + + #[inline] + pub const fn from_raw(raw: u16) -> Self {{ + Self(raw) + }} + + #[inline] + pub const fn as_raw(self) -> u16 {{ + self.0 + }} + + #[inline] + pub const fn is_unknown(self) -> bool {{ + self.0 == Self::UNKNOWN.0 + }} +}} + +impl fmt::Debug for TokenKind {{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ + let name = match *self {{ +{chr(10).join(debug_arms)} + _ => return write!(f, "TokenKind({{}})", self.0), + }}; + f.write_str(name) + }} +}} +""" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate Rust TokenKind from slang tokenkinds.txt") + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + + output = "\n".join( + [ + "// This file is generated by crates/slang-sys/scripts/generate_token_kind.py.", + "// Do not edit by hand.", + "", + "use std::fmt;", + "", + render_token_kind(load_token_kind_names(args.input)), + ] + ) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(output) + + +if __name__ == "__main__": + main() diff --git a/crates/slang-sys/scripts/generate_trivia_kind.py b/crates/slang-sys/scripts/generate_trivia_kind.py new file mode 100644 index 000000000..51ad98c8f --- /dev/null +++ b/crates/slang-sys/scripts/generate_trivia_kind.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import argparse +import re +from pathlib import Path + + +def screaming_snake(name: str) -> str: + parts = re.findall(r"[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+", name) + return "_".join(part.upper() for part in parts) + + +def load_trivia_kind_names(path: Path) -> list[str]: + kinds = [] + for raw_line in path.read_text().splitlines(): + line = raw_line.strip() + if not line or line.startswith("//"): + continue + kinds.append(line) + return kinds + + +def render_trivia_kind(kinds: list[str]) -> str: + constants = [] + debug_arms = [] + all_values = [] + + for index, kind in enumerate(kinds): + const_name = screaming_snake(kind) + constants.append(f" pub const {const_name}: Self = Self({index});") + debug_arms.append(f' Self::{const_name} => "{kind}",') + all_values.append(f"Self::{const_name}") + + return f"""#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct TriviaKind(u8); + +impl TriviaKind {{ +{chr(10).join(constants)} + + pub const ALL: &'static [Self] = &[{", ".join(all_values)}]; + + #[inline] + pub const fn from_raw(raw: u8) -> Self {{ + Self(raw) + }} + + #[inline] + pub const fn as_raw(self) -> u8 {{ + self.0 + }} + + #[inline] + pub const fn is_unknown(self) -> bool {{ + self.0 == Self::UNKNOWN.0 + }} +}} + +impl fmt::Debug for TriviaKind {{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {{ + let name = match *self {{ +{chr(10).join(debug_arms)} + _ => return write!(f, "TriviaKind({{}})", self.0), + }}; + f.write_str(name) + }} +}} +""" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate Rust TriviaKind from slang triviakinds.txt") + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + + output = "\n".join( + [ + "// This file is generated by crates/slang-sys/scripts/generate_trivia_kind.py.", + "// Do not edit by hand.", + "", + "use std::fmt;", + "", + render_trivia_kind(load_trivia_kind_names(args.input)), + ] + ) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(output) + + +if __name__ == "__main__": + main() diff --git a/crates/slang-sys/src/diagnostic.rs b/crates/slang-sys/src/diagnostic.rs new file mode 100644 index 000000000..5f91c1abb --- /dev/null +++ b/crates/slang-sys/src/diagnostic.rs @@ -0,0 +1,9 @@ +// TODO: Now our generated diagnostic struct follow the data structure in slang, +// for example, DiagCode(DiagSubsystem::Lexer, index). We mixed up all the +// diagnostic from different subsystem into one enum. This may confused rust +// compiler. So we may need to split the diagnostic into different enum for each +// subsystem. +mod slang_diagnostic { + include!(concat!(env!("OUT_DIR"), "/diagnostic.rs")); +} +pub use slang_diagnostic::*; diff --git a/crates/slang-sys/src/ffi.rs b/crates/slang-sys/src/ffi.rs new file mode 100644 index 000000000..5dc446803 --- /dev/null +++ b/crates/slang-sys/src/ffi.rs @@ -0,0 +1,16 @@ +#![allow(non_snake_case)] +#![allow(clippy::module_inception)] +#![allow(clippy::too_many_arguments)] + +#[cxx::bridge(namespace = "slang_sys")] +mod slang_ffi { + unsafe extern "C++" { + include!("wrapper.h"); + + fn parse_root_kind(text: &str) -> u16; + } +} + +pub fn parse_root_kind(text: &str) -> u16 { + slang_ffi::parse_root_kind(text) +} diff --git a/crates/slang-sys/src/lib.rs b/crates/slang-sys/src/lib.rs new file mode 100644 index 000000000..e0399edc0 --- /dev/null +++ b/crates/slang-sys/src/lib.rs @@ -0,0 +1,48 @@ +pub mod diagnostic; +pub mod ffi; +pub mod syntax; +pub mod token; + +pub fn parse_root_kind(text: &str) -> syntax::SyntaxKind { + syntax::SyntaxKind::from_raw(ffi::parse_root_kind(text)) +} + +#[cfg(test)] +mod tests { + use crate::{diagnostic, syntax}; + #[test] + fn rust_calls_upstream_slang_parser() { + let test_verilog_code = r#" +module demo( + input wire a, + output wire b +); +begin + assign b = a; +end +endmodule + "#; + let root_kind = crate::parse_root_kind(test_verilog_code); + + assert_eq!(root_kind, syntax::SyntaxKind::MODULE_DECLARATION); + } + + #[test] + fn generated_diagnostic_metadata_matches_slang_definitions() { + let expected_expression = diagnostic::DiagCode::EXPECTED_EXPRESSION.info().unwrap(); + assert_eq!(expected_expression.name, "ExpectedExpression"); + assert_eq!(expected_expression.subsystem, diagnostic::DiagSubsystem::General); + assert_eq!(expected_expression.severity, diagnostic::DiagnosticSeverity::Error); + assert_eq!(expected_expression.default_message, "expected expression"); + assert_eq!(expected_expression.option_name, None); + + let unknown_escape_code = diagnostic::DiagCode::UNKNOWN_ESCAPE_CODE.info().unwrap(); + assert_eq!(unknown_escape_code.subsystem, diagnostic::DiagSubsystem::Lexer); + assert_eq!(unknown_escape_code.severity, diagnostic::DiagnosticSeverity::Warning); + assert_eq!(unknown_escape_code.option_name, Some("unknown-escape-code")); + + let default_group = + diagnostic::DIAGNOSTIC_GROUPS.iter().find(|group| group.name == "default").unwrap(); + assert!(default_group.diagnostics.contains(&diagnostic::DiagCode::UNKNOWN_ESCAPE_CODE)); + } +} diff --git a/crates/slang-sys/src/syntax.rs b/crates/slang-sys/src/syntax.rs new file mode 100644 index 000000000..c01fc74a2 --- /dev/null +++ b/crates/slang-sys/src/syntax.rs @@ -0,0 +1,4 @@ +mod slang_syntax_kind { + include!(concat!(env!("OUT_DIR"), "/syntax_kind.rs")); +} +pub use slang_syntax_kind::*; diff --git a/crates/slang-sys/src/token.rs b/crates/slang-sys/src/token.rs new file mode 100644 index 000000000..dcdeba9c9 --- /dev/null +++ b/crates/slang-sys/src/token.rs @@ -0,0 +1,8 @@ +mod slang_token_kind { + include!(concat!(env!("OUT_DIR"), "/token_kind.rs")); +} +mod slang_trivia_kind { + include!(concat!(env!("OUT_DIR"), "/trivia_kind.rs")); +} +pub use slang_token_kind::*; +pub use slang_trivia_kind::*; diff --git a/crates/slang-sys/src/wrapper.cpp b/crates/slang-sys/src/wrapper.cpp new file mode 100644 index 000000000..5cc737658 --- /dev/null +++ b/crates/slang-sys/src/wrapper.cpp @@ -0,0 +1,20 @@ +#include "wrapper.h" + +#include + +#include "slang/syntax/SyntaxTree.h" + +namespace slang_sys +{ + +uint16_t parse_root_kind(rust::Str text) +{ + auto source = std::string_view(text.data(), text.size()); + auto name = std::string_view("demo"); + auto path = std::string_view("demo.sv"); + + auto tree = slang::syntax::SyntaxTree::fromText(source, name, path); + return static_cast(tree->root().kind); +} + +} // namespace slang_sys diff --git a/crates/slang-sys/src/wrapper.h b/crates/slang-sys/src/wrapper.h new file mode 100644 index 000000000..d608af712 --- /dev/null +++ b/crates/slang-sys/src/wrapper.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "cxx.h" + +namespace slang_sys +{ + +uint16_t parse_root_kind(rust::Str text); + +} // namespace slang_sys diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 22e742511..de89301e2 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] channel = "nightly-2026-05-24" profile = "minimal" +components = ["rustfmt", "clippy"] diff --git a/third_party/slang b/third_party/slang new file mode 160000 index 000000000..7ddf4059f --- /dev/null +++ b/third_party/slang @@ -0,0 +1 @@ +Subproject commit 7ddf4059f79eff508dd486eb42fd650cdf320d52