diff --git a/CHANGELOG.md b/CHANGELOG.md index 673d77a91a..6b77bc26fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.20.3 (TBD) + +- Define and implement Miden project file format ([#2510](https://github.com/0xMiden/miden-vm/pull/2510)). + ## 0.20.2 (2026-01-05) - Fix issue where decorator access was not bypassed properly in release mode ([#2529](https://github.com/0xMiden/miden-vm/pull/2529)). diff --git a/Cargo.lock b/Cargo.lock index 1dc4c2f272..198c6b612b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1605,6 +1605,24 @@ dependencies = [ "winter-utils", ] +[[package]] +name = "miden-project" +version = "0.20.2" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-test-serde-macros", + "proptest", + "proptest-derive", + "pubgrub", + "serde", + "serde-untagged", + "serde_json", + "smallvec", + "thiserror", + "toml", +] + [[package]] name = "miden-prover" version = "0.20.2" @@ -2116,6 +2134,17 @@ dependencies = [ "yansi", ] +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap", + "serde", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2160,6 +2189,20 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "pubgrub" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5df7e552bc7edd075f5783a87fbfc21d6a546e32c16985679c488c18192d83" +dependencies = [ + "indexmap", + "log", + "priority-queue", + "rustc-hash", + "thiserror", + "version-ranges", +] + [[package]] name = "quote" version = "1.0.42" @@ -2340,6 +2383,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.2.3" @@ -3066,6 +3115,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-ranges" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3595ffe225639f1e0fd8d7269dcc05d2fbfea93cfac2fea367daf1adb60aae91" +dependencies = [ + "smallvec", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index ef88a219f2..28b41ce15c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/debug-types", "crates/lib/core", "crates/mast-package", + "crates/project", "crates/test-serde-macros", "crates/test-utils", "crates/utils-core-derive", @@ -53,6 +54,7 @@ miden-debug-types = { path = "./crates/debug-types", version = "0.20", default-f miden-mast-package = { path = "./crates/mast-package", version = "0.20", default-features = false } miden-processor = { path = "./processor", version = "0.20", default-features = false } miden-test-serde-macros = { path = "./crates/test-serde-macros", default-features = false } +miden-project = { path = "./crates/project", version = "0.20", default-features = false } miden-prover = { path = "./prover", version = "0.20", default-features = false } miden-core-lib = { path = "./crates/lib/core", version = "0.20", default-features = false } miden-utils-core-derive = { path = "./crates/utils-core-derive", version = "0.20", default-features = false } @@ -68,8 +70,6 @@ miden-formatting = { version = "0.1", default-features = false } midenc-hir-type = { version = "0.4", default-features = false } # Constraints to resolve duplicates -semver = "1.0" -syn = "1.0" windows-sys = "0.61" # Third-party crates @@ -81,13 +81,26 @@ insta = { version = "1.43", default-features = false, features = ["colors"] } itertools = { version = "0.14", default-features = false, features = ["use_alloc"] } log = { version = "0.4", default-features = false } paste = { version = "1.0", default-features = false } -proptest = { version = "1.8", default-features = false, features = ["no_std", "alloc"] } +proptest = { version = "1.8", default-features = false, features = [ + "no_std", + "alloc", +] } proptest-derive = { version = "0.7", default-features = false } -serde = { version = "1.0", default-features = false, features = ["alloc", "derive", "rc"] } +serde = { version = "1.0", default-features = false, features = [ + "alloc", + "derive", + "rc", +] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } serde-untagged = {version = "0.1" } sha2 = { version = "0.10", default-features = false } -smallvec = { version = "1.15", default-features = false, features = ["union", "const_generics", "const_new"] } +smallvec = { version = "1.15", default-features = false, features = [ + "union", + "const_generics", + "const_new", +] } +semver = { version = "1.0", default-features = false } +syn = "1.0" thiserror = { version = "2.0", default-features = false } tokio = { version = "1.48", default-features = false } tracing = { version = "0.1", default-features = false, features = ["attributes"] } diff --git a/Makefile b/Makefile index 2c5a62a0f3..f9c233507a 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ WARNINGS := RUSTDOCFLAGS="-D warnings" BUILDDOCS := MIDEN_BUILD_LIB_DOCS=1 # -- feature configuration ------------------------------------------------------------------------ -ALL_FEATURES_BUT_ASYNC := --features concurrent,executable,metal,testing,internal +ALL_FEATURES_BUT_ASYNC := --features concurrent,executable,metal,testing,internal,resolver # Workspace-wide test features WORKSPACE_TEST_FEATURES := concurrent,testing,metal,executable @@ -50,8 +50,10 @@ FEATURES_assembly-syntax := testing,serde FEATURES_core := FEATURES_miden-vm := concurrent,executable,metal,internal FEATURES_processor := concurrent,testing,bus-debugger +FEATURES_project := resolver FEATURES_prover := concurrent,metal -FEATURES_core-lib :=FEATURES_verifier := +FEATURES_core-lib := +FEATURES_verifier := # -- linting -------------------------------------------------------------------------------------- diff --git a/crates/assembly-syntax/src/lib.rs b/crates/assembly-syntax/src/lib.rs index 98b3db8cab..20d3859327 100644 --- a/crates/assembly-syntax/src/lib.rs +++ b/crates/assembly-syntax/src/lib.rs @@ -9,6 +9,7 @@ extern crate std; pub use miden_core::{Felt, FieldElement, StarkField, Word, prettier, utils::DisplayHex}; pub use miden_debug_types as debuginfo; pub use miden_utils_diagnostics::{self as diagnostics, Report}; +pub use semver; #[cfg(feature = "arbitrary")] pub mod arbitrary; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml new file mode 100644 index 0000000000..f63a65c7da --- /dev/null +++ b/crates/project/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "miden-project" +version.workspace = true +description = "Interface for working with Miden projects" +documentation = "https://docs.rs/miden-project" +readme = "README.md" +categories = ["compilers", "no-std"] +keywords = ["project", "miden"] +license.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true +edition.workspace = true + +[features] +default = ["std", "serde"] +arbitrary = ["std", "dep:proptest-derive", "dep:proptest", "miden-assembly-syntax/arbitrary"] +resolver = ["std", "dep:pubgrub", "dep:smallvec"] +std = ["miden-assembly-syntax/std", "serde/std", "thiserror/std", "toml/std"] +serde = ["dep:serde", "dep:serde-untagged", "miden-assembly-syntax/serde", "miden-core/serde"] + +[dependencies] +miden-assembly-syntax.workspace = true +miden-core.workspace = true +proptest = { workspace = true, optional = true } +proptest-derive = { workspace = true, optional = true } +pubgrub = { version = "0.3", optional = true } +serde = { workspace = true, optional = true } +serde-untagged = { workspace = true, optional = true } +smallvec = { workspace = true, optional = true } +thiserror.workspace = true +toml = { version = "0.9", default-features = false, features = ["serde", "parse"] } + +[dev-dependencies] +miden-core.workspace = true +miden-test-serde-macros.workspace = true +serde_json.workspace = true +toml = { version = "0.9", default-features = false, features = ["std", "serde", "parse"] } + +[lints.rust] +# This is needed until this warning appears in stable when this is commented out. Currently, it +# appears only on nightly, as a side effect of code generated by miette. If we use +# `#[expect(unused_assignments)] in those modules, we get warnings on stable, but not on nightly. +# If we don't do that, we get no warnings on stable, but we do on nightly. Rather than mess about +# with build scripts and such, we just disable this warning for now, as it is essentially useless +# until we get the same behavior on stable and nightly +unused_assignments = "allow" diff --git a/crates/project/README.md b/crates/project/README.md new file mode 100644 index 0000000000..d898e86b33 --- /dev/null +++ b/crates/project/README.md @@ -0,0 +1,147 @@ +# miden-project + +This crate defines the interfaces for working with `miden-project.toml` files and the corresponding Miden project metadata. + +## Terminology + +The following document uses some terminology that may be easier to reason about if you understand the specific definitions we're using for those terms in this context: + +- *Package*, a single library, program, or component that can be assembled, linked against, and distributed. A package in binary form is considered _assembled_. We also refer to packages as an organizational unit, i.e. before they are assembled, we organize source code into units that correspond to the packages we intend to produce from that source code. Typically the type of package we're referring to will be clear from context, but if we need to distinguish between them, then we use _assembled package_ to refer to the binary form. For an intuition about packages, you can consider a Miden package roughly equivalent to a Rust crate. +- *Project*, refers to the meta-organization of one or more packages into a single unit. See the definition for _Workspace_ for more details on multi-package projects. +- *Workspace*, refers to a project that contains multiple packages, and can share/inherit dependencies and configuration defined at the workspace level. These work just like Cargo workspaces, if you are familiar with their semantics. +- *Version*, refers to the semantic version of a project/package +- *Digest*, refers to the content digest/hash associated with a specific assembled package. Only assembled packages have content digests. When available for a specific package, the content digest becomes an extension of its _version_, and is taken into account during dependency resolution when a specific digest is required by a dependency spec. + +## What is a Miden project? + +A Miden project, at its most basic, is comprised of two things: + +1. A manifest file (i.e. `miden-project.toml`) which describes the project and its dependencies, as well as configuration for tooling in the Miden ecosystem. +2. The source code of the project. This could be any of the following: + - Miden Assembly + - Rust, using an installed `cargo-miden`/`midenc` toolchain + - Any language that can compile to the Miden VM, specifically, are able to produce Miden packages (i.e. `.masp` files). + - Some combination of the above in one project + +The above is the simplest form of project, oriented towards building a single Miden package. However, Miden projects may also be organized into workspaces, similar to how Cargo supports organizing Rust crates into workspaces. + +## Workspaces + +A Miden workspace is a meta-project that consists of one or more sub-projects that correspond to packages. A workspace is comprised of: + +* A workspace-level manifest (i.e. `miden-project.toml`), with workspace-specific syntax, that defines configuration and metadata for the workspace, and is shared amongst members of the workspace. +* One or more subdirectories that contain a Miden project corresponding to a single package. These are the "members" of the workspace. + +The benefit of using workspaces is when working with multiple package projects that depend on each other, or which share configuration - as opposed to managing them all independently, which would require much more duplication. + +### Defining a workspace + +To define a workspace, simply create a new directory, and in that directory, +create a `miden-project.toml` file which contains the following: + +```toml +[workspace] +members = [] + +[workspace.package] +version = "0.1.0" + +[dependencies] +``` + +This represents an empty workspace for a new project at version `0.1.0`. + +The next step is to create subdirectories for each sub-project, and initialize those as called for by the tooling of the language used in that project. + +For examples of Miden Assembly and Rust-based projects, see the section below titled [Defining a project](#defining-a-project). + +## Defining a project + +To define a new project, all you need is a new directory containing the source code of the project (organization of which is language-specific), and in that directory, create a `miden-project.toml` which contains at least the following: + +```toml +[package] +name = "name" +version = "0.1.0" # or whatever semantic version you like +``` + +This provides the bare minimum metadata needed to construct a basic package. However, in practice you are likely going to want to define [_targets_](#defining-targets), and declare [_dependencies_](#dependency-management). + +### Defining targets + +A _target_ corresponds to a specific artifact that you want to produce from the project. Most projects will have a single target, but in some cases multiple targets may be useful, particularly for kernels. + +Let's add a target to the `miden-project.toml` of the project we created in the previous section: + +```toml +[package] +name = "name" +version = "0.1.0" + +# The following target is what would be inferred if no targets were declared +# in this file, also known as the _default target_. +[[target]] +kind = "lib" # the type of artifact we're producing +path = "mod.masm" # the relative path to the root module +namespace = "name" # the root namespace of modules parsed for this target +``` + +As noted above, we've added a target definition that is equivalent to the default target that is inferred if no targets are explicitly declared: a library, whose root module is expected to be found in `mod.masm`, and whose modules will all belong to the `name` namespace (individual modules will have +their path derived from the directory structure). + +There are other types of targets though, currently the available kinds are: + +* `library`, `lib` - produce a package which exports one or more procedures that can be called from other artifacts, but cannot be executed by the Miden VM without additional setup. Libraries have no implicit dependency on any particular kernel. +* `executable`, `bin`, or `program` - produce a package which has a single entrypoint, and can be executed by the Miden VM directly. Executables have no dependency on any particular kernel. +* `account-component` - produce a package which is a valid account component in the Miden protocol, and contains all metadata needed to construct that component. This type is only valid in conjunction with the Miden transaction kernel. +* `note-script` - produce a package which is a valid note script in the Miden protocol, and exports the necessary metadata and procedures to construct and execute the note. This type is only valid in conjunction with the Miden transaction kernel. +* `tx-script` - produce a package which is a valid transaction script in the Miden protocol, and exports the necessary metadata and procedures to construct and execute the script. This type is only valid in conjunction with the Miden transaction kernel. + +As noted earlier, you may define multiple targets in a single Miden project - however you must then request a specific target when assembling the project. + +### Dependency management + +A key benefit of Miden project manifests is the ability to declare dependencies on Miden packages, and then use those packages in your project, without having to manage the complexity of working with the contents of those packages yourself. + +Dependencies are declared in `miden-project.toml` in one of the following forms: + +```toml +[dependencies] +# A semantic version constraint +a = "=0.1.0" +# A specific package, given by its content digest +b = "0x......" +# A path dependency +c = { path = "../c" } +d = { path = "../c", version = "~> 0.1.0" } +# A git dependency +e = { git = "https://github.com/example/e", branch = "main" } +f = { git = "https://github.com/example/f", rev = "deadbeef" } +g = { git = "https://github.com/example/g", rev = "deadbeef", version = "~> 0.1.0" } +``` + +#### Semantics + +* `a` specifies a semantic version requirement, these would be evaluated against a package registry implementation +* `b` specifies a package digest, essentially this acts as a stricter SemVer requirement of the form `=MAJ.MIN.PATCH`, where what is required is a version that has exactly the given digest. This form is also evaluated against a package registry implementation. +* `c` specifies that the package sources (or a package artifact) can be found at the given path. The version is inferred from the package at that path, but is essentially equivalent to `version = "*"`. +* `d` is the same as `c`, except it specifies a semantic version requirement that _MUST_ match the package found at `path` +* `e` specifies that the package sources can be found by cloning the `git` repo, and checking out the `main` branch. +* `f` is the same as `e`, except it provides a specific revision in the `git` repo instead +* `g` is the same as `f`, except it specifies a semantic version requirement that _MUST_ match the package found in the cloned repo + +In cases where the dependency is resolved to project sources and _not_ an assembled package, the behavior would be to assemble those dependencies first, and then link against them when assembling the current project. This is most useful when linking against packages which are _not_ contracts, or where the contracts are deployed together as a unit. + +**NOTE:** Currently there is no canonical package registry, so the resolution of the first two forms described above is dependent on the specific tool that is doing the resolution, namely, how it populates the package index for the resolver provided by this crate. + +### Build profiles + +TODO + +### Custom package metadata + +TODO + +### Lint configuration + +TODO diff --git a/crates/project/examples/multi_target/kernel/mod.masm b/crates/project/examples/multi_target/kernel/mod.masm new file mode 100644 index 0000000000..d47dd58c96 --- /dev/null +++ b/crates/project/examples/multi_target/kernel/mod.masm @@ -0,0 +1,34 @@ +const SYSCALL_TABLE_ADDR = 0 +const NUM_SYSCALLS = 2 +const PRINTLN = event("sys.println") +const PRINTLN_ADDR = SYSCALL_TABLE_ADDR + 4 + +#! Causes execution to trap +pub proc panic + push.0 assert +end + +#! Prints the given null-terminated byte string to stdout, if debug tracing is enabled +pub proc println(message: ptr) + trace.PRINTLN + drop +end + +#! Execute one of this kernel's syscalls +#! +#! This allows downstream packages to link against the kernel, without requiring them to be +#! recompiled every time the kernel changes. +pub proc system_call + # validate syscall id + dup.0 lt.NUM_SYSCALLS assert + # invoke the syscall + mul.4 add.SYSCALL_TABLE_ADDR mem_loadw dynexec +end + +begin + # At program start, initialize the syscall table + procref.panic + mem_storew.SYSCALL_TABLE_ADDR + procref.println + mem_storew.PRINTLN_ADDR +end diff --git a/crates/project/examples/multi_target/miden-project.toml b/crates/project/examples/multi_target/miden-project.toml new file mode 100644 index 0000000000..c4e40465c2 --- /dev/null +++ b/crates/project/examples/multi_target/miden-project.toml @@ -0,0 +1,12 @@ +[package] +name = "my-kernel" +version = "1.0.0" + +[[target]] +kind = "kernel" +path = "kernel/mod.masm" + +[[target]] +kind = "library" +path = "lib/mod.masm" +namespace = "userspace" diff --git a/crates/project/examples/multi_target/userspace/mod.masm b/crates/project/examples/multi_target/userspace/mod.masm new file mode 100644 index 0000000000..440e6196a8 --- /dev/null +++ b/crates/project/examples/multi_target/userspace/mod.masm @@ -0,0 +1,9 @@ +pub proc panic + push.0 + syscall.system_call +end + +pub proc println(message: ptr) + push.1 + syscall.system_call +end diff --git a/crates/project/examples/package/lib/foo.masm b/crates/project/examples/package/lib/foo.masm new file mode 100644 index 0000000000..c3a1244dc7 --- /dev/null +++ b/crates/project/examples/package/lib/foo.masm @@ -0,0 +1,3 @@ +pub proc explode + push.0 assert +end diff --git a/crates/project/examples/package/lib/mod.masm b/crates/project/examples/package/lib/mod.masm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/project/examples/package/miden-project.toml b/crates/project/examples/package/miden-project.toml new file mode 100644 index 0000000000..51be4ea26c --- /dev/null +++ b/crates/project/examples/package/miden-project.toml @@ -0,0 +1,20 @@ +[package] +name = "example" +version = "0.1.0" + +[[target]] +kind = "library" +path = "lib/mod.masm" + +[dependencies] +my-kernel = { path = "../multi_target/userspace" } + +[profile.test] +inherits = "dev" +network = "local" + +[lints.miden] +unused = "error" + +[package.metadata.network.local] +endpoint = "tcp://127.0.0.1" diff --git a/crates/project/examples/workspace/account/miden-project.toml b/crates/project/examples/workspace/account/miden-project.toml new file mode 100644 index 0000000000..91cd56e9a1 --- /dev/null +++ b/crates/project/examples/workspace/account/miden-project.toml @@ -0,0 +1,6 @@ +[package] +name = "my-account" +version = "0.1.0" + +[[target]] +kind = "account-component" diff --git a/crates/project/examples/workspace/account/mod.masm b/crates/project/examples/workspace/account/mod.masm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/project/examples/workspace/miden-project.toml b/crates/project/examples/workspace/miden-project.toml new file mode 100644 index 0000000000..9a9beaa2fd --- /dev/null +++ b/crates/project/examples/workspace/miden-project.toml @@ -0,0 +1,11 @@ +[workspace] +members = [ + "account", + "note" +] + +[workspace.dependencies] +my-account = { path = "account" } + +[profile.test] +inherits = "dev" diff --git a/crates/project/examples/workspace/note/miden-project.toml b/crates/project/examples/workspace/note/miden-project.toml new file mode 100644 index 0000000000..45327cb917 --- /dev/null +++ b/crates/project/examples/workspace/note/miden-project.toml @@ -0,0 +1,11 @@ + +[package] +name = "my-account-note" +version = "0.1.0" + +[[target]] +kind = "note" +namespace = "my_account::note" + +[dependencies] +my-account.workspace = true diff --git a/crates/project/examples/workspace/note/mod.masm b/crates/project/examples/workspace/note/mod.masm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/project/src/ast.rs b/crates/project/src/ast.rs new file mode 100644 index 0000000000..583beadf8d --- /dev/null +++ b/crates/project/src/ast.rs @@ -0,0 +1,170 @@ +//! This module and its children define the abstract syntax tree representation of the +//! `miden-project.toml` file and its variants (i.e. workspace-level vs package-level). +//! +//! The AST is used for parsing and rendering the TOML representation, but after validation and +//! resolution of inherited properties, the AST is translated to a simpler structure that does not +//! need to represent the complexity of the on-disk format. +mod dependency; +mod package; +pub(crate) mod parsing; +mod profile; +mod target; +mod workspace; + +use alloc::{ + boxed::Box, + format, + string::{String, ToString}, + sync::Arc, + vec, + vec::Vec, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +pub use self::{ + dependency::DependencySpec, + package::{PackageConfig, PackageDetail, PackageFile}, + profile::Profile, + target::Target, + workspace::WorkspaceFile, +}; +use crate::{Diagnostic, Label, RelatedError, Report, SourceFile, SourceSpan, miette}; + +/// Represents all possible variants of `miden-project.toml` +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged, rename_all = "lowercase"))] +pub enum MidenProject { + /// A workspace-level configuration file. + /// + /// On its own, a workspace-level `miden-project.toml` does define a package, instead packages + /// are derived from the members of the workspace. + Workspace(Box), + /// A package-level configuration file. + /// + /// A `miden-project.toml` of this variety defines a package, and may reference/override any + /// workspace-level dependencies, lints, or build profiles. + Package(Box), +} + +/// Accessors +impl MidenProject { + /// Returns true if this project is actually a multi-project workspace + pub fn is_workspace(&self) -> bool { + matches!(self, Self::Workspace(_)) + } +} + +/// Parsing +#[cfg(feature = "serde")] +impl MidenProject { + /// Parse a [MidenProject] from the provided TOML source file, generally `miden-project.toml` + /// + /// If successful, the contents of the manifest are semantically valid, with the following + /// caveats: + /// + /// * If parsing a workspace-level configuration, the workspace members are not checked, so it + /// is up to the caller to iterate over the member paths, and parse/validate their respective + /// configurations. + /// * If parsing an individual project configuration which belongs to a workspace, inherited + /// properties from the workspace-level are assumed to exist and be correct. It is up to the + /// caller to compute the concrete property values and validate them at that point. + pub fn parse(source: Arc) -> Result { + if source.as_str().contains("[workspace") { + Ok(Self::Workspace(Box::new(WorkspaceFile::parse(source)?))) + } else { + Ok(Self::Package(Box::new(PackageFile::parse(source)?))) + } + } +} + +/// An internal error type used when parsing a `miden-project.toml` file. +#[allow(dead_code)] // Different feature combinations may produce dead variants +#[derive(Debug, thiserror::Error, Diagnostic)] +pub(crate) enum ProjectFileError { + #[error("unable to parse project manifest: {message}")] + ParseError { + message: String, + #[source_code] + source_file: Arc, + #[label(primary)] + span: SourceSpan, + }, + #[error("invalid project name")] + #[diagnostic(help("The project name must be a valid Miden Assembly namespace identifier"))] + InvalidProjectName { + #[source_code] + source_file: Arc, + #[label(primary)] + label: Label, + }, + #[error("invalid workspace dependency specification")] + InvalidWorkspaceDependency { + #[source_code] + source_file: Arc, + #[label(primary)] + label: Label, + }, + #[error("invalid dependency specification")] + InvalidPackageDependency { + #[source_code] + source_file: Arc, + #[label(primary)] + label: Label, + }, + #[error("invalid build target configuration")] + InvalidBuildTargets { + #[source_code] + source_file: Arc, + #[related] + related: Vec, + }, + #[error("package is not a member of a workspace")] + NotAWorkspace { + #[source_code] + source_file: Arc, + #[label(primary)] + span: SourceSpan, + }, + #[error("failed to load workspace member")] + LoadWorkspaceMemberFailed { + #[source_code] + source_file: Arc, + #[label(primary)] + span: Label, + }, + #[error("no profile named '{name}' has been defined yet")] + UnknownProfile { + name: Arc, + #[source_code] + source_file: Arc, + #[label(primary)] + span: SourceSpan, + }, + #[error("cannot redefine profile '{name}'")] + DuplicateProfile { + name: Arc, + #[source_code] + source_file: Arc, + #[label(primary)] + span: SourceSpan, + #[label] + prev: SourceSpan, + }, + #[error("missing required field 'version'")] + MissingVersion { + #[source_code] + source_file: Arc, + #[label(primary)] + span: SourceSpan, + }, + #[error("workspace does not define 'version'")] + MissingWorkspaceVersion { + #[source_code] + source_file: Arc, + #[label(primary)] + span: SourceSpan, + }, +} diff --git a/crates/project/src/ast/dependency.rs b/crates/project/src/ast/dependency.rs new file mode 100644 index 0000000000..99d2922cb2 --- /dev/null +++ b/crates/project/src/ast/dependency.rs @@ -0,0 +1,152 @@ +use super::{parsing::SetSourceId, *}; +use crate::{SourceId, Span, Uri, VersionRequirement}; + +/// Represents information about a project dependency needed to resolve it to a Miden package +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct DependencySpec { + /// The name of the dependency package + #[cfg_attr(feature = "serde", serde(default, skip))] + pub name: Span>, + /// The version requirement specified for this dependency + #[cfg_attr( + feature = "serde", + serde(rename = "version", alias = "digest", skip_serializing_if = "Option::is_none") + )] + pub version_or_digest: Option, + /// Whether or not the version requirement is inherited from the containing workspace + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "does_not_inherit_from_workspace") + )] + pub workspace: bool, + /// If present, specifies the path from which this dependency should be loaded + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub path: Option>, + /// If present, specifies the URI of the git repository to clone in order to load this + /// dependency. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub git: Option>, + /// If present, specifies the branch of the git repository to checkout when loading this + /// dependency from the URI specified by `git`. + /// + /// NOTE: This field is only valid when specified along with `git`, and may not be used in + /// conjunction with `rev`. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub branch: Option>>, + /// If present, specifies the revision of the git repository to checkout when loading this + /// dependency from the URI specified by `git`. + /// + /// NOTE: This field is only valid when specified along with `git`, and may not be used in + /// conjunction with `branch`. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub rev: Option>>, +} + +#[inline(always)] +fn does_not_inherit_from_workspace(is_workspace_dependency: &bool) -> bool { + !(*is_workspace_dependency) +} + +impl DependencySpec { + /// Returns the version constraint to apply to this dependency + pub fn version(&self) -> Option<&VersionRequirement> { + self.version_or_digest.as_ref() + } + + /// Returns true if this dependency inherits its version requirement from a parent workspace + pub fn inherits_workspace_version(&self) -> bool { + self.workspace + } + + /// Returns true if this dependency must be resolved using a host-provided resolver + pub fn is_host_resolved(&self) -> bool { + self.git.is_none() && self.path.is_none() + } + + /// Returns true if this dependency specifies a local filesystem path + pub fn is_path(&self) -> bool { + self.path.is_some() && self.git.is_none() + } + + /// Returns true if this dependency specifies a git repository + pub fn is_git(&self) -> bool { + self.git.is_some() + } +} + +impl SetSourceId for DependencySpec { + fn set_source_id(&mut self, source_id: SourceId) { + self.name.set_source_id(source_id); + if let Some(version_or_digest) = self.version_or_digest.as_mut() { + version_or_digest.set_source_id(source_id); + } + + if let Some(path) = self.path.as_mut() { + path.set_source_id(source_id); + } + + if let Some(git) = self.git.as_mut() { + git.set_source_id(source_id); + } + + if let Some(branch) = self.branch.as_mut() { + branch.set_source_id(source_id); + } + + if let Some(rev) = self.rev.as_mut() { + rev.set_source_id(source_id); + } + } +} + +#[cfg(feature = "serde")] +pub use self::serialization::deserialize_dependency_map; + +#[cfg(feature = "serde")] +mod serialization { + use alloc::sync::Arc; + + use miden_assembly_syntax::debuginfo::Span; + use serde::de::{MapAccess, Visitor}; + + use super::DependencySpec; + use crate::Map; + + struct DependencyMapVisitor; + + impl<'de> Visitor<'de> for DependencyMapVisitor { + type Value = Map>, Span>; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a dependency map") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = Self::Value::default(); + + while let Some((key, mut value)) = + access.next_entry::>, Span>()? + { + value.name = key.clone(); + map.insert(key, value); + } + + Ok(map) + } + } + + #[allow(clippy::type_complexity)] + pub fn deserialize_dependency_map<'de, D>( + deserializer: D, + ) -> Result>, Span>, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(DependencyMapVisitor) + } +} diff --git a/crates/project/src/ast/package.rs b/crates/project/src/ast/package.rs new file mode 100644 index 0000000000..e32388c195 --- /dev/null +++ b/crates/project/src/ast/package.rs @@ -0,0 +1,313 @@ +use alloc::collections::BTreeMap; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::{ + parsing::{MaybeInherit, SetSourceId, Validate}, + *, +}; +use crate::{Map, MetadataSet, RelatedLabel, SemVer, SourceId, Span, TargetType, Uri}; + +/// Represents the contents of the `[package]` table +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct PackageTable { + /// The name of this package + pub name: Span>, + /// Additional package information, optionally inheritable from a parent workspace (if present) + #[cfg_attr(feature = "serde", serde(flatten))] + pub detail: PackageDetail, +} + +impl SetSourceId for PackageTable { + fn set_source_id(&mut self, source_id: SourceId) { + let Self { name, detail } = self; + name.set_source_id(source_id); + detail.set_source_id(source_id); + } +} + +/// Package properties which may be inherited from a parent workspace +#[derive(Default, Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct PackageDetail { + /// The semantic version assigned to this package + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub version: Option>>, + /// An (optional) brief description of this project + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub description: Option>>>, + /// Custom metadata which can be used by third-party/downstream tooling + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Map::is_empty"))] + pub metadata: MetadataSet, +} + +impl SetSourceId for PackageDetail { + fn set_source_id(&mut self, source_id: SourceId) { + let Self { version, description, metadata } = self; + if let Some(version) = version.as_mut() { + version.set_source_id(source_id); + } + if let Some(description) = description.as_mut() { + description.set_source_id(source_id); + } + metadata.set_source_id(source_id); + } +} + +/// Package configuration which can be defined at both the workspace and package level +#[derive(Default, Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct PackageConfig { + /// The set of dependencies required by this package/workspace + #[cfg_attr( + feature = "serde", + serde( + default, + deserialize_with = "dependency::deserialize_dependency_map", + skip_serializing_if = "Map::is_empty" + ) + )] + pub dependencies: Map>, Span>, + /// Linter configuration/overrides for this package/workspace + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Map::is_empty"))] + pub lints: MetadataSet, +} + +impl SetSourceId for PackageConfig { + fn set_source_id(&mut self, source_id: SourceId) { + let Self { dependencies, lints } = self; + dependencies.set_source_id(source_id); + lints.set_source_id(source_id); + } +} + +/// Represents the `miden-project.toml` structure of an individual package +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct PackageFile { + /// The original source file this was parsed from, if applicable/known + #[cfg_attr(feature = "serde", serde(skip, default))] + pub source_file: Option>, + /// Contents of the `[package]` table + pub package: PackageTable, + /// Contents of tables shared with workspace-level `miden-project.toml`, e.g. `[dependencies]` + /// and `[lints]` + #[cfg_attr(feature = "serde", serde(flatten))] + pub config: PackageConfig, + /// The set of build targets defined in this file + #[cfg_attr( + feature = "serde", + serde(default, rename = "target", skip_serializing_if = "Vec::is_empty") + )] + pub targets: Vec>, + /// The set of build profiles defined in this file + #[cfg_attr( + feature = "serde", + serde( + default, + rename = "profile", + deserialize_with = "super::profile::deserialize_profiles_table", + skip_serializing_if = "Vec::is_empty" + ) + )] + pub profiles: Vec, +} + +/// Parsing +#[cfg(feature = "serde")] +impl PackageFile { + /// Parse a [PackageFile] from the provided TOML source file, generally `miden-project.toml` + /// + /// If successful, the contents of the manifest are semantically valid, with the following + /// caveats: + /// + /// * Inherited properties from the workspace-level are assumed to exist and be correct. It is + /// up to the caller to compute the concrete property values and validate them at that point. + pub fn parse(source: Arc) -> Result { + use parsing::{SetSourceId, Validate}; + + let source_id = source.id(); + + // Parse the unvalidated project from source + let mut package = toml::from_str::(source.as_str()).map_err(|err| { + let span = err + .span() + .map(|span| { + let start = span.start as u32; + let end = span.end as u32; + SourceSpan::new(source_id, start..end) + }) + .unwrap_or_default(); + Report::from(ProjectFileError::ParseError { + message: err.message().to_string(), + source_file: source.clone(), + span, + }) + })?; + + package.source_file = Some(source.clone()); + package.set_source_id(source_id); + package.validate(source)?; + + Ok(package) + } +} + +impl SetSourceId for PackageFile { + fn set_source_id(&mut self, source_id: SourceId) { + let Self { + source_file: _, + package, + config, + targets, + profiles, + } = self; + package.set_source_id(source_id); + config.set_source_id(source_id); + targets.set_source_id(source_id); + profiles.set_source_id(source_id); + } +} + +/// An internal error type for representing information about build target conflicts +#[derive(Debug, thiserror::Error, Diagnostic)] +#[error("build target conflicts found")] +struct TargetConflictError { + #[label] + label: Label, + #[label(collection)] + conflicts: Vec