Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions cli/golem-cli/wit/deps/golem-core-v2/golem-core-v2.wit
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,16 @@ interface types {

// --- Primitives ---
bool-type,
s8-type,
s16-type,
s32-type,
s64-type,
u8-type,
u16-type,
u32-type,
u64-type,
f32-type,
f64-type,
s8-type(option<numeric-restrictions>),
s16-type(option<numeric-restrictions>),
s32-type(option<numeric-restrictions>),
s64-type(option<numeric-restrictions>),
u8-type(option<numeric-restrictions>),
u16-type(option<numeric-restrictions>),
u32-type(option<numeric-restrictions>),
u64-type(option<numeric-restrictions>),
f32-type(option<numeric-restrictions>),
f64-type(option<numeric-restrictions>),
char-type,
string-type,

Expand Down Expand Up @@ -301,6 +301,27 @@ interface types {
err: option<type-node-index>,
}

// --- Numeric restrictions ---

/// A numeric bound usable across every numeric representation. Float bounds
/// carry canonical IEEE-754 `f64` bits (NaN/inf rejected, -0.0 normalized);
/// comparisons decode the bits to `f64` and compare numerically.
variant numeric-bound {
signed(s64),
unsigned(u64),
float-bits(u64),
}

/// Inline numeric refinement. `none` on a numeric type means unconstrained
/// (the common case). The empty restriction set is never encoded as `some`:
/// producers normalize it to `none`, decoders normalize a decoded empty to
/// `none`. `unit` is schema/help metadata only.
record numeric-restrictions {
min: option<numeric-bound>,
max: option<numeric-bound>,
unit: option<string>,
}

// --- Text / Binary restrictions ---

record text-restrictions {
Expand Down
40 changes: 30 additions & 10 deletions golem-api-grpc/proto/golem/schema/schema.proto
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,16 @@ message SchemaType {

// Primitives
golem.common.Empty bool_type = 3;
golem.common.Empty s8_type = 4;
golem.common.Empty s16_type = 5;
golem.common.Empty s32_type = 6;
golem.common.Empty s64_type = 7;
golem.common.Empty u8_type = 8;
golem.common.Empty u16_type = 9;
golem.common.Empty u32_type = 10;
golem.common.Empty u64_type = 11;
golem.common.Empty f32_type = 12;
golem.common.Empty f64_type = 13;
NumericRestrictions s8_type = 4;
NumericRestrictions s16_type = 5;
NumericRestrictions s32_type = 6;
NumericRestrictions s64_type = 7;
NumericRestrictions u8_type = 8;
NumericRestrictions u16_type = 9;
NumericRestrictions u32_type = 10;
NumericRestrictions u64_type = 11;
NumericRestrictions f32_type = 12;
NumericRestrictions f64_type = 13;
golem.common.Empty char_type = 14;
golem.common.Empty string_type = 15;

Expand Down Expand Up @@ -162,6 +162,26 @@ message ResultType {
SchemaType err = 2;
}

// A numeric bound usable across every numeric representation. Float bounds
// carry canonical IEEE-754 f64 bits (NaN/inf rejected, -0.0 normalized).
message NumericBound {
oneof bound {
int64 signed = 1;
uint64 unsigned = 2;
uint64 float_bits = 3;
}
}

// Inline numeric refinement. A present message with no fields set decodes to
// `None` (the empty restriction set is never kept as `Some`). `unit` is
// schema/help metadata only.
message NumericRestrictions {
// Absent min/max (message not set) means unbounded on that side.
NumericBound min = 1;
NumericBound max = 2;
optional string unit = 3;
}

message TextRestrictions {
// Absent `languages` means unrestricted.
StringList languages = 1;
Expand Down
21 changes: 21 additions & 0 deletions golem-common/src/schema/tests/binary_codec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,24 @@ proptest! {
);
}
}

/// Deterministic numeric-restriction vectors round-trip through the desert
/// `BinaryCodec` exactly. Adjacent payloadless variants (`bool`/`char`/`string`)
/// are included to prove the codec stays internally consistent after the numeric
/// variants gained an `Option<NumericRestrictions>` payload.
#[test]
fn numeric_restrictions_binary_codec_golden_round_trip() {
use crate::schema::schema_type::SchemaType;

for (label, ty) in crate::schema::tests::golden_numeric_schema_types() {
let graph = SchemaGraph::anonymous(ty);
let back: SchemaGraph = roundtrip(&graph);
assert_eq!(graph, back, "binary numeric golden mismatch: {label}");
}

for adjacent in [SchemaType::bool(), SchemaType::char(), SchemaType::string()] {
let graph = SchemaGraph::anonymous(adjacent);
let back: SchemaGraph = roundtrip(&graph);
assert_eq!(graph, back);
}
}
96 changes: 96 additions & 0 deletions golem-common/src/schema/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,99 @@ mod protobuf_tests;
mod schema_derive_tests;
#[cfg(feature = "full")]
mod wit_tests;

/// Deterministic cross-SDK numeric-restriction vectors, shared by the protobuf,
/// WIT, and binary codec round-trip tests. Every entry is already normalized
/// (no `Some(empty)`), so each codec must preserve it exactly. Empty → `None`
/// normalization is covered separately by the per-codec decode-boundary tests.
#[cfg(feature = "full")]
pub(crate) fn golden_numeric_schema_types()
-> Vec<(&'static str, crate::schema::schema_type::SchemaType)> {
use crate::schema::metadata::MetadataEnvelope;
use crate::schema::schema_type::{NumericBound, NumericRestrictions, SchemaType};

fn restr(
min: Option<NumericBound>,
max: Option<NumericBound>,
unit: Option<&str>,
) -> Option<NumericRestrictions> {
NumericRestrictions {
min,
max,
unit: unit.map(|u| u.to_string()),
}
.normalize()
}

macro_rules! numeric {
($variant:ident, $r:expr) => {
SchemaType::$variant {
restrictions: $r,
metadata: MetadataEnvelope::default(),
}
};
}

let u = NumericBound::Unsigned;
let s = NumericBound::Signed;
let f = |v: f64| NumericBound::float(v).expect("finite bound");

vec![
// bare u32 (None) — the unconstrained hot-path case
("u32 bare", SchemaType::u32()),
// u32 min=1
("u32 min=1", numeric!(U32, restr(Some(u(1)), None, None))),
(
"u32 min=1 +unit",
numeric!(U32, restr(Some(u(1)), None, Some("items"))),
),
// u32 bounds=(0,100)
(
"u32 bounds=(0,100)",
numeric!(U32, restr(Some(u(0)), Some(u(100)), None)),
),
(
"u32 bounds=(0,100) +unit",
numeric!(U32, restr(Some(u(0)), Some(u(100)), Some("percent"))),
),
// s64 bounds=(0, i64::MAX)
(
"s64 bounds=(0,i64::MAX)",
numeric!(S64, restr(Some(s(0)), Some(s(i64::MAX)), None)),
),
(
"s64 bounds=(0,i64::MAX) +unit",
numeric!(S64, restr(Some(s(0)), Some(s(i64::MAX)), Some("ns"))),
),
// u64 near u64::MAX
(
"u64 near u64::MAX",
numeric!(U64, restr(Some(u(u64::MAX - 1)), Some(u(u64::MAX)), None)),
),
(
"u64 near u64::MAX +unit",
numeric!(
U64,
restr(Some(u(u64::MAX - 1)), Some(u(u64::MAX)), Some("bytes"))
),
),
// f64 min=0.0
(
"f64 min=0.0",
numeric!(F64, restr(Some(f(0.0)), None, None)),
),
(
"f64 min=0.0 +unit",
numeric!(F64, restr(Some(f(0.0)), None, Some("seconds"))),
),
// s8 / f32 coverage for the desert variant-payload change
(
"s8 bounds=(-1,1)",
numeric!(S8, restr(Some(s(-1)), Some(s(1)), None)),
),
(
"f32 max=1.5",
numeric!(F32, restr(None, Some(f(1.5)), None)),
),
]
}
37 changes: 37 additions & 0 deletions golem-common/src/schema/tests/protobuf_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,40 @@ fn field_source_round_trips() {
assert_eq!(source, back);
}
}

/// Deterministic numeric-restriction vectors round-trip through the protobuf
/// mirror exactly.
#[test]
fn numeric_restrictions_proto_golden_round_trip() {
for (label, ty) in crate::schema::tests::golden_numeric_schema_types() {
let graph = SchemaGraph::anonymous(ty);
let proto: golem_api_grpc::proto::golem::schema::SchemaGraph = graph.clone().into();
let back: SchemaGraph = proto.try_into().expect("decode");
assert_eq!(graph, back, "proto numeric golden mismatch: {label}");
}
}

/// A stored `Some(empty)` numeric restriction normalizes to `None` when it
/// crosses the protobuf decode boundary (covers both `unit: None` and the
/// empty-string `unit` case).
#[test]
fn numeric_empty_restrictions_normalize_to_none_proto() {
use crate::schema::schema_type::{NumericRestrictions, SchemaType};

for empty in [
NumericRestrictions::default(),
NumericRestrictions {
min: None,
max: None,
unit: Some(String::new()),
},
] {
let graph = SchemaGraph::anonymous(SchemaType::U32 {
restrictions: Some(empty),
metadata: MetadataEnvelope::default(),
});
let proto: golem_api_grpc::proto::golem::schema::SchemaGraph = graph.into();
let back: SchemaGraph = proto.try_into().expect("decode");
assert_eq!(back.root.numeric_restrictions(), None);
}
}
47 changes: 46 additions & 1 deletion golem-common/src/schema/tests/wit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,56 @@ fn unknown_ref_is_rejected_on_encode() {
assert!(matches!(err, EncodeError::UnknownTypeId(_)));
}

/// Deterministic numeric-restriction vectors round-trip through the WIT codec
/// exactly.
#[test]
fn numeric_restrictions_wit_golden_round_trip() {
for (label, ty) in crate::schema::tests::golden_numeric_schema_types() {
let graph = SchemaGraph::anonymous(ty);
let wire = encode_graph(&graph).expect("encode");
let back = decode_graph(&wire).expect("decode");
assert_eq!(graph, back, "wit numeric golden mismatch: {label}");
}
}

/// A `some(empty)` numeric restriction on the WIT wire normalizes to `None` when
/// decoded, both through the native encode path and a hand-built wire payload
/// carrying an empty-string `unit`.
#[test]
fn numeric_empty_restrictions_normalize_to_none_wit() {
use crate::schema::schema_type::{NumericRestrictions, SchemaType};

// Native Some(empty) encodes as some(empty wire) and decodes back to None.
let graph = SchemaGraph::anonymous(SchemaType::U32 {
restrictions: Some(NumericRestrictions::default()),
metadata: Default::default(),
});
let wire = encode_graph(&graph).expect("encode");
let back = decode_graph(&wire).expect("decode");
assert_eq!(back.root.numeric_restrictions(), None);

// A hand-built wire payload with an empty-string unit also normalizes away.
let wire_graph = wire::SchemaGraph {
type_nodes: vec![wire::SchemaTypeNode {
body: wire::SchemaTypeBody::U32Type(Some(wire::NumericRestrictions {
min: None,
max: None,
unit: Some(String::new()),
})),
metadata: empty_metadata(),
}],
defs: vec![],
root: 0,
};
let back = decode_graph(&wire_graph).expect("decode");
assert_eq!(back.root.numeric_restrictions(), None);
}

#[test]
fn duplicate_def_id_is_rejected_on_decode() {
let graph = wire::SchemaGraph {
type_nodes: vec![wire::SchemaTypeNode {
body: wire::SchemaTypeBody::S32Type,
body: wire::SchemaTypeBody::S32Type(None),
metadata: empty_metadata(),
}],
defs: vec![
Expand Down
36 changes: 31 additions & 5 deletions golem-common/src/schema/tool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ pub struct Positional {
/// Default value, interpreted against [`type_`](Self::type_).
pub default: Option<SchemaValue>,
pub required: bool,
/// If true, the positional's value may be read from standard input.
pub accepts_stdio: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand All @@ -154,6 +156,8 @@ pub struct TailPositional {
pub separator: Option<String>,
/// If true, tokens after `separator` are not flag-parsed.
pub verbatim: bool,
/// If true, the tail items may be read from standard input.
pub accepts_stdio: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand All @@ -176,15 +180,37 @@ pub enum OptionShape {
Scalar(SchemaType),
/// Bare presence collapses to `default`; with value parses normally.
OptionalScalar(SchemaType),
/// Repeatable; value type in the derived signature is list-of-scalar.
Repeatable(RepeatableShape),
/// Repeatable scalar option (`-e a -e b`); the collected value is a list
/// of the element type.
RepeatableList(RepeatableListShape),
/// Repeatable key-value option (`-c a=1 -c b=2`); the collected value is a
/// [`SchemaType::Map`], never a `list<tuple>`.
RepeatableMap(RepeatableMapShape),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RepeatableShape {
pub struct RepeatableListShape {
pub repetition: Repetition,
/// Schema of a single item.
pub type_: SchemaType,
/// Schema of a single collected element.
pub item_type: SchemaType,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RepeatableMapShape {
pub repetition: Repetition,
/// A [`SchemaType::Map`] node; the collected value is this map.
pub map_type: SchemaType,
/// What happens when the same key is supplied more than once.
pub duplicate_key_policy: DuplicateKeyPolicy,
}

/// Resolution policy for a repeated key in a [`OptionShape::RepeatableMap`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DuplicateKeyPolicy {
/// A repeated key is a usage error.
Reject,
/// A repeated key takes the last supplied value.
LastWins,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
Loading
Loading