Skip to content

Commit

Permalink
Add schema::type_for_generate, update validation to use it, fixing a
Browse files Browse the repository at this point in the history
minor bug in the tests in the process
  • Loading branch information
kazimuth committed Sep 3, 2024
1 parent 8ee1de6 commit cc22212
Show file tree
Hide file tree
Showing 11 changed files with 705 additions and 231 deletions.
1 change: 1 addition & 0 deletions crates/lib/src/db/raw_def/v9.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ pub struct RawTypeDefV9 {
pub name: RawScopedTypeNameV9,

/// The type to which the declaration refers.
/// This must point to an `AlgebraicType::Product` or an `AlgebraicType::Sum` in the module's typespace.
pub ty: AlgebraicTypeRef,

/// Whether this type has a custom ordering.
Expand Down
16 changes: 16 additions & 0 deletions crates/sats/src/algebraic_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,22 @@ impl AlgebraicType {
matches!(self, Self::Sum(p) if p.is_schedule_at())
}

/// Returns whether this type is a unit type.
pub fn is_unit(&self) -> bool {
matches!(self, Self::Product(p) if p.is_unit())
}

/// Returns whether this type is a never type.
pub fn is_never(&self) -> bool {
matches!(self, Self::Sum(p) if p.is_empty())
}

/// If this type is the standard option type, returns the type of the `some` variant.
/// Otherwise, returns `None`.
pub fn as_option(&self) -> Option<&AlgebraicType> {
self.as_sum().and_then(SumType::as_option)
}

/// Returns whether this type is scalar or a string type.
pub fn is_scalar_or_string(&self) -> bool {
self.is_scalar() || self.is_string()
Expand Down
20 changes: 17 additions & 3 deletions crates/sats/src/proptest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! This notably excludes `Ref` types.

use crate::{i256, u256};
use crate::{i256, u256, ProductTypeElement, SumTypeVariant};
use crate::{
AlgebraicType, AlgebraicTypeRef, AlgebraicValue, ArrayValue, MapType, MapValue, ProductType, ProductValue, SumType,
SumValue, Typespace, F32, F64,
Expand Down Expand Up @@ -54,15 +54,29 @@ fn generate_algebraic_type_from_leaves(
prop_oneof![
gen_element.clone().prop_map(AlgebraicType::array),
(gen_element.clone(), gen_element.clone()).prop_map(|(key, val)| AlgebraicType::map(key, val)),
// No need for field or variant names.

// No need to generate units here;
// we already generate them in `generate_non_compound_algebraic_type`.
vec(gen_element.clone().prop_map_into(), 1..=SIZE)
.prop_map(|vec| vec
.into_iter()
.enumerate()
.map(|(i, ty)| ProductTypeElement {
name: Some(format!("element_{i}").into()),
algebraic_type: ty
})
.collect())
.prop_map(Vec::into_boxed_slice)
.prop_map(AlgebraicType::product),
// Do not generate nevers here; we can't store never in a page.
vec(gen_element.clone().prop_map_into(), 1..=SIZE)
.prop_map(|vec| vec
.into_iter()
.enumerate()
.map(|(i, ty)| SumTypeVariant {
name: Some(format!("variant_{i}").into()),
algebraic_type: ty
})
.collect::<Vec<_>>())
.prop_map(Vec::into_boxed_slice)
.prop_map(AlgebraicType::sum),
]
Expand Down
8 changes: 8 additions & 0 deletions crates/sats/src/typespace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ impl Typespace {
Ok(())
}

/// Iterate over types in the typespace with their references.
pub fn refs_with_types(&self) -> impl Iterator<Item = (AlgebraicTypeRef, &AlgebraicType)> {
self.types
.iter()
.enumerate()
.map(|(idx, ty)| (AlgebraicTypeRef(idx as _), ty))
}

/// Check that the entire typespace is valid for generating a `SpacetimeDB` client module.
/// See also the `spacetimedb_schema` crate, which layers additional validation on top
/// of these checks.
Expand Down
7 changes: 7 additions & 0 deletions crates/schema/proptest-regressions/type_for_generate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 50cf163ac81228385b27f96ba1801355e39bc722a937a0cf6ec0d4b27d23ef14 # shrinks to t = Typespace { types: [Bool, Bool, Bool, Product(ProductType { elements: [ProductTypeElement { name: None, algebraic_type: Bool }] }), Bool] }
18 changes: 18 additions & 0 deletions crates/schema/src/def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use std::hash::Hash;

use crate::error::{IdentifierError, ValidationErrors};
use crate::identifier::Identifier;
use crate::type_for_generate::{AlgebraicTypeUse, TypespaceForGenerate};
use hashbrown::Equivalent;
use itertools::Itertools;
use spacetimedb_data_structures::error_stream::{CollectAllErrors, CombineErrors, ErrorStream};
Expand Down Expand Up @@ -88,6 +89,9 @@ pub struct ModuleDef {
/// The typespace of the module definition.
typespace: Typespace,

/// The typespace, restructured to be useful for client codegen.
typespace_for_generate: TypespaceForGenerate,

/// The global namespace of the module:
/// tables, indexes, constraints, schedules, and sequences live in the global namespace.
/// Concretely, though, they're stored in the `TableDef` data structures.
Expand Down Expand Up @@ -128,6 +132,11 @@ impl ModuleDef {
&self.typespace
}

/// The typespace of the module from a different perspective, one useful for client code generation.
pub fn typespace_for_generate(&self) -> &TypespaceForGenerate {
&self.typespace_for_generate
}

/// The `TableDef` an entity in the global namespace is stored in, if any.
///
/// Generally, you will want to use the `lookup` method on the entity type instead.
Expand Down Expand Up @@ -260,6 +269,7 @@ impl From<ModuleDef> for RawModuleDefV9 {
types,
typespace,
stored_in_table_def: _,
typespace_for_generate: _,
} = val;

RawModuleDefV9 {
Expand Down Expand Up @@ -514,6 +524,9 @@ pub struct ColumnDef {
/// with name `Some(name.as_str())`.
pub ty: AlgebraicType,

/// The type of the column, formatted for client code generation.
pub ty_for_generate: AlgebraicTypeUse,

/// The table this `ColumnDef` is stored in.
pub table_name: Identifier,
}
Expand Down Expand Up @@ -579,6 +592,8 @@ pub struct TypeDef {
pub name: ScopedTypeName,

/// The type to which the alias refers.
/// Look in `ModuleDef.typespace` for the actual type,
/// or in `ModuleDef.typespace_for_generate` for the client codegen version.
pub ty: AlgebraicTypeRef,

/// Whether this type has a custom ordering.
Expand Down Expand Up @@ -687,6 +702,9 @@ pub struct ReducerDef {
/// This `ProductType` need not be registered in the module's `Typespace`.
pub params: ProductType,

/// The parameters of the reducer, formatted for client codegen.
pub params_for_generate: Vec<AlgebraicTypeUse>,

/// The special role of this reducer in the module lifecycle, if any.
pub lifecycle: Option<Lifecycle>,
}
Expand Down
73 changes: 35 additions & 38 deletions crates/schema/src/def/validate/v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,12 +362,12 @@ mod tests {
use crate::def::validate::v8::{IDENTITY_CONNECTED_NAME, IDENTITY_DISCONNECTED_NAME, INIT_NAME};
use crate::def::{validate::Result, ModuleDef};
use crate::error::*;
use crate::type_for_generate::ClientCodegenError;

use spacetimedb_data_structures::expect_error_matching;
use spacetimedb_lib::db::raw_def::*;
use spacetimedb_lib::{ScheduleAt, TableDesc};
use spacetimedb_primitives::{ColId, ColList, Constraints};
use spacetimedb_sats::typespace::TypeRefError;
use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, ProductType};
use v8::RawModuleDefV8Builder;
use v9::Lifecycle;
Expand Down Expand Up @@ -703,51 +703,39 @@ mod tests {
let recursive_type = AlgebraicType::product([("a", AlgebraicTypeRef(0).into())]);

let mut builder = RawModuleDefV8Builder::default();
builder.add_type_for_tests("Recursive", recursive_type.clone());
builder.add_reducer_for_tests("silly", ProductType::from([("a", recursive_type.clone())]));
let ref_ = builder.add_type_for_tests("Recursive", recursive_type.clone());
builder.add_reducer_for_tests("silly", ProductType::from([("a", ref_.into())]));
let result: Result<ModuleDef> = builder.finish().try_into();

// If you use a recursive type as a reducer argument, you get two errors.
// One for the reducer argument, and one for the type itself.
// This seems fine...
expect_error_matching!(result, ValidationError::ResolutionFailure { location, ty, error } => {
location == &TypeLocation::InTypespace { ref_: AlgebraicTypeRef(0) } &&
ty.0 == recursive_type &&
error == &TypeRefError::RecursiveTypeRef(AlgebraicTypeRef(0))
});
expect_error_matching!(result, ValidationError::ResolutionFailure { location, ty, error } => {
expect_error_matching!(result, ValidationError::ClientCodegenError { location, error: ClientCodegenError::TypeRefError(_) } => {
location == &TypeLocation::ReducerArg {
reducer_name: "silly".into(),
position: 0,
arg_name: Some("a".into())
} &&
ty.0 == recursive_type &&
error == &TypeRefError::RecursiveTypeRef(AlgebraicTypeRef(0))
}
});
expect_error_matching!(result, ValidationError::ClientCodegenError { location, error: ClientCodegenError::TypeRefError(_) } => {
location == &TypeLocation::InTypespace { ref_: AlgebraicTypeRef(0) }
});
}

#[test]
fn invalid_type_ref() {
let invalid_type_1 = AlgebraicType::product([("a", AlgebraicTypeRef(31).into())]);
let invalid_type_2 = AlgebraicType::option(AlgebraicTypeRef(55).into());
let mut builder = RawModuleDefV8Builder::default();
builder.add_type_for_tests("Invalid", invalid_type_1.clone());
builder.add_reducer_for_tests("silly", ProductType::from([("a", invalid_type_2.clone())]));
let ref_ = builder.add_type_for_tests("Invalid", invalid_type_1.clone());
builder.add_reducer_for_tests("silly", ProductType::from([("a", ref_.into())]));
let result: Result<ModuleDef> = builder.finish().try_into();

expect_error_matching!(result, ValidationError::ResolutionFailure { location, ty, error } => {
location == &TypeLocation::InTypespace { ref_: AlgebraicTypeRef(0) } &&
ty.0 == invalid_type_1 &&
error == &TypeRefError::InvalidTypeRef(AlgebraicTypeRef(31))
});
expect_error_matching!(result, ValidationError::ResolutionFailure { location, ty, error } => {
expect_error_matching!(result, ValidationError::ClientCodegenError { location, error: ClientCodegenError::TypeRefError(_) } => {
location == &TypeLocation::ReducerArg {
reducer_name: "silly".into(),
position: 0,
arg_name: Some("a".into())
} &&
ty.0 == invalid_type_2 &&
error == &TypeRefError::InvalidTypeRef(AlgebraicTypeRef(55))
}
});
expect_error_matching!(result, ValidationError::ClientCodegenError { location, error: ClientCodegenError::TypeRefError(_) } => {
location == &TypeLocation::InTypespace { ref_: AlgebraicTypeRef(0) }
});
}

Expand All @@ -756,22 +744,31 @@ mod tests {
let inner_type_invalid_for_use = AlgebraicType::product([("b", AlgebraicType::U32)]);
let invalid_type = AlgebraicType::product([("a", inner_type_invalid_for_use.clone())]);
let mut builder = RawModuleDefV8Builder::default();
builder.add_type_for_tests("Invalid", invalid_type.clone());
builder.add_reducer_for_tests("silly", ProductType::from([("a", invalid_type.clone())]));
let ref_ = builder.add_type_for_tests("Invalid", invalid_type.clone());
builder.add_reducer_for_tests("silly", ProductType::from([("a", ref_.into())]));
let result: Result<ModuleDef> = builder.finish().try_into();

expect_error_matching!(result, ValidationError::NotValidForTypeDefinition { ref_, ty } => {
ref_ == &AlgebraicTypeRef(0) &&
ty == &invalid_type
});
expect_error_matching!(result, ValidationError::NotValidForTypeUse { location, ty } => {
location == &TypeLocation::ReducerArg {
expect_error_matching!(
result,
ValidationError::ClientCodegenError {
location,
error: ClientCodegenError::NonSpecialTypeNotAUse { ty }
} => {
location == &TypeLocation::InTypespace { ref_: AlgebraicTypeRef(0) } &&
ty.0 == inner_type_invalid_for_use
}
);
expect_error_matching!(
result,
ValidationError::ClientCodegenError {
location,
error: ClientCodegenError::NonSpecialTypeNotAUse { .. }
} => location == &TypeLocation::ReducerArg {
reducer_name: "silly".into(),
position: 0,
arg_name: Some("a".into())
} &&
ty.0 == invalid_type
});
}
);
}

#[test]
Expand Down
Loading

0 comments on commit cc22212

Please sign in to comment.