Skip to content

Commit b653c99

Browse files
committed
feat(vortex-edition): session-registered editions with declaration macros
Editions live on the session like encodings do: EditionSession is a session variable holding the per-session registry of editions and edition inclusions, populated explicitly at initialization time. EditionSession::default() seeds the first-party declarations — core2026.07.0 (a draft until its min_vortex_version is recorded, which is the act of freezing) and the 32 inclusions for the encodings the default writer emits — and any crate can declare further inclusions into a session, so declarations can migrate next to each encoding's registration. Two proc macros produce compile-time-validated declarations without any registration side effects: edition! parses a "family+year.month.version" identifier into a pub const EditionId, and edition_inclusion!("vortex.alp" in CORE_2026_07_0) expands to a const-usable EditionInclusion value. The session computes everything else: editions() lists the registry, encodings_in(edition) resolves an edition's full set (membership is inherited by later editions of the family), current(family) picks the newest frozen edition, and validate() rejects undeclared references, drafts preceding frozen editions, malformed versions, and members requiring a release newer than their edition declares. The intended core2026.07.0 set is pinned by a golden test. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KdR42Svu74NcNzC7XJYwLr
1 parent 39f4abe commit b653c99

9 files changed

Lines changed: 898 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ members = [
88
"vortex-mask",
99
"vortex-utils",
1010
"vortex-session",
11+
"vortex-edition",
12+
"vortex-edition-macros",
1113
"vortex-flatbuffers",
1214
"vortex-metrics",
1315
"vortex-io",
@@ -295,6 +297,8 @@ vortex-compute = { version = "0.1.0", path = "./vortex-compute", default-feature
295297
vortex-datafusion = { version = "0.1.0", path = "./vortex-datafusion", default-features = false }
296298
vortex-datetime-parts = { version = "0.1.0", path = "./encodings/datetime-parts", default-features = false }
297299
vortex-decimal-byte-parts = { version = "0.1.0", path = "encodings/decimal-byte-parts", default-features = false }
300+
vortex-edition = { version = "0.1.0", path = "./vortex-edition", default-features = false }
301+
vortex-edition-macros = { version = "0.1.0", path = "./vortex-edition-macros" }
298302
vortex-error = { version = "0.1.0", path = "./vortex-error", default-features = false }
299303
vortex-fastlanes = { version = "0.1.0", path = "./encodings/fastlanes", default-features = false }
300304
vortex-file = { version = "0.1.0", path = "./vortex-file", default-features = false }

vortex-edition-macros/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "vortex-edition-macros"
3+
authors = { workspace = true }
4+
categories = { workspace = true }
5+
description = "Proc macros for declaring Vortex editions and edition inclusions"
6+
edition = { workspace = true }
7+
homepage = { workspace = true }
8+
include = { workspace = true }
9+
keywords = { workspace = true }
10+
license = { workspace = true }
11+
readme = { workspace = true }
12+
repository = { workspace = true }
13+
rust-version = { workspace = true }
14+
version = { workspace = true }
15+
16+
[lints]
17+
workspace = true
18+
19+
[lib]
20+
proc-macro = true
21+
22+
[dependencies]
23+
proc-macro2 = { workspace = true }
24+
quote = { workspace = true }
25+
syn = { workspace = true }

vortex-edition-macros/src/lib.rs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Proc macros for declaring Vortex editions and edition inclusions.
5+
//!
6+
//! Use these through the re-exports in `vortex-edition`; the expansions reference
7+
//! `::vortex_edition` paths. The macros only produce validated declarations — registration
8+
//! happens explicitly against the session's `EditionSession`.
9+
10+
use proc_macro::TokenStream;
11+
use quote::quote;
12+
use syn::Attribute;
13+
use syn::Ident;
14+
use syn::LitStr;
15+
use syn::Path;
16+
use syn::Token;
17+
use syn::Visibility;
18+
use syn::parse::Parse;
19+
use syn::parse::ParseStream;
20+
use syn::parse_macro_input;
21+
22+
/// Declare an edition.
23+
///
24+
/// ```ignore
25+
/// vortex_edition::edition! {
26+
/// /// The first edition of the `core` family.
27+
/// pub CORE_2026_07_0 = "core2026.07.0";
28+
/// }
29+
/// ```
30+
///
31+
/// The identifier string is `family` + `year.month.version` and is parsed at compile time.
32+
/// Expands to a `const` `EditionId`; the edition itself is registered with
33+
/// `EditionSession::declare_edition`, where recording a `min_vortex_version` freezes it.
34+
#[proc_macro]
35+
pub fn edition(input: TokenStream) -> TokenStream {
36+
let decl = parse_macro_input!(input as EditionDecl);
37+
match expand_edition(decl) {
38+
Ok(tokens) => tokens.into(),
39+
Err(err) => err.to_compile_error().into(),
40+
}
41+
}
42+
43+
/// Declare that an encoding is included in an edition (and all later editions of the same
44+
/// family).
45+
///
46+
/// ```ignore
47+
/// vortex_edition::edition_inclusion!("vortex.alp" in CORE_2026_07_0);
48+
/// ```
49+
///
50+
/// An optional `required_vortex_release = "x.y.z"` records the earliest release able to
51+
/// read the encoding. The encoding ID is validated at compile time
52+
/// (`namespace.name`, lowercase). Expands to an `EditionInclusion` value, usable in const
53+
/// contexts; register it with `EditionSession::declare_inclusion`.
54+
#[proc_macro]
55+
pub fn edition_inclusion(input: TokenStream) -> TokenStream {
56+
let decl = parse_macro_input!(input as InclusionDecl);
57+
match expand_inclusion(decl) {
58+
Ok(tokens) => tokens.into(),
59+
Err(err) => err.to_compile_error().into(),
60+
}
61+
}
62+
63+
struct EditionDecl {
64+
attrs: Vec<Attribute>,
65+
vis: Visibility,
66+
name: Ident,
67+
id: LitStr,
68+
}
69+
70+
impl Parse for EditionDecl {
71+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
72+
let attrs = input.call(Attribute::parse_outer)?;
73+
let vis: Visibility = input.parse()?;
74+
let name: Ident = input.parse()?;
75+
input.parse::<Token![=]>()?;
76+
let id: LitStr = input.parse()?;
77+
if input.peek(Token![;]) {
78+
input.parse::<Token![;]>()?;
79+
}
80+
Ok(Self {
81+
attrs,
82+
vis,
83+
name,
84+
id,
85+
})
86+
}
87+
}
88+
89+
fn expand_edition(decl: EditionDecl) -> syn::Result<proc_macro2::TokenStream> {
90+
let (family, year, month, version) = parse_edition_id(&decl.id)?;
91+
92+
let EditionDecl {
93+
attrs, vis, name, ..
94+
} = decl;
95+
96+
Ok(quote! {
97+
#(#attrs)*
98+
#vis const #name: ::vortex_edition::EditionId =
99+
::vortex_edition::EditionId::new(#family, #year, #month, #version);
100+
})
101+
}
102+
103+
struct InclusionDecl {
104+
encoding_id: LitStr,
105+
edition: Path,
106+
required_vortex_release: Option<LitStr>,
107+
}
108+
109+
impl Parse for InclusionDecl {
110+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
111+
let encoding_id: LitStr = input.parse()?;
112+
input.parse::<Token![in]>()?;
113+
let edition: Path = input.parse()?;
114+
let mut required_vortex_release = None;
115+
if input.peek(Token![,]) {
116+
input.parse::<Token![,]>()?;
117+
let key: Ident = input.parse()?;
118+
if key != "required_vortex_release" {
119+
return Err(syn::Error::new(
120+
key.span(),
121+
"expected `required_vortex_release`",
122+
));
123+
}
124+
input.parse::<Token![=]>()?;
125+
required_vortex_release = Some(input.parse()?);
126+
}
127+
Ok(Self {
128+
encoding_id,
129+
edition,
130+
required_vortex_release,
131+
})
132+
}
133+
}
134+
135+
fn expand_inclusion(decl: InclusionDecl) -> syn::Result<proc_macro2::TokenStream> {
136+
validate_encoding_id(&decl.encoding_id)?;
137+
if let Some(release) = &decl.required_vortex_release {
138+
validate_release(release)?;
139+
}
140+
141+
let InclusionDecl {
142+
encoding_id,
143+
edition,
144+
required_vortex_release,
145+
} = decl;
146+
let required = match required_vortex_release {
147+
Some(release) => quote!(::core::option::Option::Some(#release)),
148+
None => quote!(::core::option::Option::None),
149+
};
150+
151+
Ok(quote! {
152+
::vortex_edition::EditionInclusion {
153+
encoding_id: #encoding_id,
154+
since: #edition,
155+
required_vortex_release: #required,
156+
}
157+
})
158+
}
159+
160+
/// Parse `family` + `year.month.version`, e.g. `core2026.07.0`.
161+
fn parse_edition_id(lit: &LitStr) -> syn::Result<(String, u16, u8, u8)> {
162+
let value = lit.value();
163+
let err = |msg: &str| syn::Error::new(lit.span(), format!("invalid edition id: {msg}"));
164+
165+
let family: String = value
166+
.chars()
167+
.take_while(|c| c.is_ascii_lowercase())
168+
.collect();
169+
if family.is_empty() {
170+
return Err(err("must start with a lowercase family name, e.g. `core`"));
171+
}
172+
173+
let rest = &value[family.len()..];
174+
let parts: Vec<&str> = rest.split('.').collect();
175+
let [year, month, version] = parts.as_slice() else {
176+
return Err(err("expected `family` + `year.month.version`"));
177+
};
178+
if year.len() != 4 || month.len() != 2 {
179+
return Err(err(
180+
"expected a 4-digit year and 2-digit month, e.g. `core2026.07.0`",
181+
));
182+
}
183+
let year: u16 = year.parse().map_err(|_| err("year is not a number"))?;
184+
let month: u8 = month.parse().map_err(|_| err("month is not a number"))?;
185+
let version: u8 = version
186+
.parse()
187+
.map_err(|_| err("version is not a number"))?;
188+
if month == 0 || month > 12 {
189+
return Err(err("month must be 01-12"));
190+
}
191+
Ok((family, year, month, version))
192+
}
193+
194+
/// Validate an encoding ID: lowercase `namespace.name` with `[a-z0-9._-]` characters.
195+
fn validate_encoding_id(lit: &LitStr) -> syn::Result<()> {
196+
let value = lit.value();
197+
let valid = !value.starts_with('.')
198+
&& !value.ends_with('.')
199+
&& value.contains('.')
200+
&& value
201+
.chars()
202+
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || "._-".contains(c));
203+
if !valid {
204+
return Err(syn::Error::new(
205+
lit.span(),
206+
"invalid encoding id: expected lowercase `namespace.name`, e.g. `vortex.alp`",
207+
));
208+
}
209+
Ok(())
210+
}
211+
212+
/// Validate a `major.minor.patch` release string.
213+
fn validate_release(lit: &LitStr) -> syn::Result<()> {
214+
let value = lit.value();
215+
let valid = value.split('.').count() == 3
216+
&& value
217+
.split('.')
218+
.all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit()));
219+
if !valid {
220+
return Err(syn::Error::new(
221+
lit.span(),
222+
"invalid release: expected `major.minor.patch`, e.g. `0.70.0`",
223+
));
224+
}
225+
Ok(())
226+
}

vortex-edition/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "vortex-edition"
3+
authors.workspace = true
4+
description = "Definitions of Vortex editions: named, frozen sets of encodings with a read-compatibility guarantee"
5+
edition = { workspace = true }
6+
homepage = { workspace = true }
7+
categories = { workspace = true }
8+
include = { workspace = true }
9+
keywords = { workspace = true }
10+
license = { workspace = true }
11+
readme = { workspace = true }
12+
repository = { workspace = true }
13+
rust-version = { workspace = true }
14+
version = { workspace = true }
15+
16+
[package.metadata.docs.rs]
17+
all-features = true
18+
19+
[lints]
20+
workspace = true
21+
22+
[dependencies]
23+
parking_lot = { workspace = true }
24+
vortex-edition-macros = { workspace = true }
25+
vortex-session = { workspace = true }

vortex-edition/src/definitions.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! The first-party edition and inclusion declarations, seeded into
5+
//! [`EditionSession::default`](crate::EditionSession).
6+
//!
7+
//! The computed set of a frozen edition is pinned by a golden test in this crate, so any
8+
//! change to these declarations that alters a frozen set fails CI. New encodings are staged
9+
//! into the newest draft edition. Inclusions are declared centrally here for now; they are
10+
//! expected to migrate next to each encoding's registration.
11+
12+
use crate::Edition;
13+
use crate::EditionInclusion;
14+
use crate::edition;
15+
use crate::edition_inclusion;
16+
17+
edition! {
18+
/// The first edition of the `core` family: the encodings the default file writer emits.
19+
/// A draft until the release shipping it is published and its version is recorded.
20+
pub CORE_2026_07_0 = "core2026.07.0";
21+
}
22+
23+
/// The first-party editions.
24+
pub static DEFAULT_EDITIONS: &[Edition] = &[Edition {
25+
id: CORE_2026_07_0,
26+
// TODO(editions): freeze by setting this to the first release shipping this edition,
27+
// once it is published.
28+
min_vortex_version: None,
29+
}];
30+
31+
/// The first-party edition inclusions.
32+
pub static DEFAULT_INCLUSIONS: &[EditionInclusion] = &[
33+
edition_inclusion!("vortex.null" in CORE_2026_07_0),
34+
edition_inclusion!("vortex.bool" in CORE_2026_07_0),
35+
edition_inclusion!("vortex.primitive" in CORE_2026_07_0),
36+
edition_inclusion!("vortex.decimal" in CORE_2026_07_0),
37+
edition_inclusion!("vortex.varbin" in CORE_2026_07_0),
38+
edition_inclusion!("vortex.varbinview" in CORE_2026_07_0),
39+
edition_inclusion!("vortex.list" in CORE_2026_07_0),
40+
edition_inclusion!("vortex.listview" in CORE_2026_07_0),
41+
edition_inclusion!("vortex.fixed_size_list" in CORE_2026_07_0),
42+
edition_inclusion!("vortex.struct" in CORE_2026_07_0),
43+
edition_inclusion!("vortex.variant" in CORE_2026_07_0),
44+
edition_inclusion!("vortex.ext" in CORE_2026_07_0),
45+
edition_inclusion!("vortex.chunked" in CORE_2026_07_0),
46+
edition_inclusion!("vortex.constant" in CORE_2026_07_0),
47+
edition_inclusion!("vortex.dict" in CORE_2026_07_0),
48+
edition_inclusion!("vortex.masked" in CORE_2026_07_0),
49+
edition_inclusion!("vortex.sparse" in CORE_2026_07_0),
50+
edition_inclusion!("vortex.alp" in CORE_2026_07_0),
51+
edition_inclusion!("vortex.alprd" in CORE_2026_07_0),
52+
edition_inclusion!("vortex.bytebool" in CORE_2026_07_0),
53+
edition_inclusion!("vortex.datetimeparts" in CORE_2026_07_0),
54+
edition_inclusion!("vortex.decimal_byte_parts" in CORE_2026_07_0),
55+
edition_inclusion!("vortex.fsst" in CORE_2026_07_0),
56+
edition_inclusion!("vortex.pco" in CORE_2026_07_0),
57+
edition_inclusion!("vortex.runend" in CORE_2026_07_0),
58+
edition_inclusion!("vortex.sequence" in CORE_2026_07_0),
59+
edition_inclusion!("vortex.zigzag" in CORE_2026_07_0),
60+
edition_inclusion!("vortex.zstd" in CORE_2026_07_0),
61+
edition_inclusion!("fastlanes.bitpacked" in CORE_2026_07_0),
62+
edition_inclusion!("fastlanes.delta" in CORE_2026_07_0),
63+
edition_inclusion!("fastlanes.for" in CORE_2026_07_0),
64+
edition_inclusion!("fastlanes.rle" in CORE_2026_07_0),
65+
];

0 commit comments

Comments
 (0)