Skip to content

Commit f0d369a

Browse files
committed
Support spread operator on types
1 parent 659cd2f commit f0d369a

File tree

9 files changed

+881
-79
lines changed

9 files changed

+881
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
### Changed
1212

13-
- Replaced tuple update syntax with spread operator (`...`) for more flexible/intuitive merging/updating of tuples.
13+
- Replaced tuple update syntax with spread operator (`...`) for more flexible/intuitive merging/updating of tuples for both values and types.
1414
- Replaced `type` keyword with `::` syntax (e.g., `point :: Point[x: int, y: int]`).
1515
- Changed syntax for calling built-ins to `__add__`.
1616

docs/spec.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,30 @@ json ::
114114
| Array[(Nil | Cons[&, &1])];
115115
```
116116

117+
### Type spreads
118+
119+
Type aliases can be extended using the spread operator `...` to compose new types from existing ones:
120+
121+
```
122+
// Compose types from reusable pieces
123+
entity :: [id: int, created_at: int];
124+
updateable :: (updated_at: int);
125+
post :: Post[...entity, title: Str[bin], ...updatable]; // Post[id: int, created_at: int, title: Str[bin], updated_at: int]
126+
127+
// Field override - later fields override earlier ones
128+
v1 :: User[id: int, name: Str[bin]];
129+
v2 :: v1[..., id: bin]; // User[id: bin, name: Str[bin]]
130+
```
131+
132+
When spreading a union type, the spread is distributed across all variants:
133+
134+
```
135+
event :: Created[id: int] | Updated | Deleted;
136+
logged :: event[..., timestamp: int] // Created[id: int, timestamp: int] | Updated[timestamp: int] | Deleted[timestamp: int];
137+
138+
// Expands to: Traced[value: bin, request_id: bin] | Traced[message: bin, request_id: bin]
139+
```
140+
117141
### Strings
118142

119143
The compiler converts UTF-8 strings, defined with `"..."` into binaries, wrapped in a `Str` tuple (`Str['...']`).

quiver-compiler/src/ast.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,19 @@ pub enum Literal {
7272
Binary(Vec<u8>),
7373
}
7474

75+
#[derive(Debug, Clone, PartialEq)]
76+
pub enum TupleName {
77+
/// Explicit tuple name like Point[x: 1]
78+
Literal(String),
79+
/// Inherit name from variable like a[..., y: 2]
80+
Identifier(String),
81+
/// Unnamed tuple like [x: 1]
82+
None,
83+
}
84+
7585
#[derive(Debug, Clone, PartialEq)]
7686
pub struct Tuple {
77-
pub name: Option<String>,
87+
pub name: TupleName,
7888
pub fields: Vec<TupleField>,
7989
}
8090

@@ -179,15 +189,21 @@ pub enum PrimitiveType {
179189

180190
#[derive(Debug, Clone, PartialEq)]
181191
pub struct TupleType {
182-
pub name: Option<String>,
192+
pub name: TupleName,
183193
pub fields: Vec<FieldType>,
184194
pub is_partial: bool,
185195
}
186196

187197
#[derive(Debug, Clone, PartialEq)]
188-
pub struct FieldType {
189-
pub name: Option<String>,
190-
pub type_def: Type,
198+
pub enum FieldType {
199+
Field {
200+
name: Option<String>,
201+
type_def: Type,
202+
},
203+
Spread {
204+
identifier: Option<String>,
205+
type_arguments: Vec<Type>,
206+
},
191207
}
192208

193209
#[derive(Debug, Clone, PartialEq)]

quiver-compiler/src/compiler.rs

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod variables;
1313
pub use codegen::InstructionBuilder;
1414
pub use modules::{ModuleCache, compile_type_import};
1515
pub use pattern::MatchCertainty;
16-
pub use typing::{TupleAccessor, TypeAliasDef, union_types};
16+
pub use typing::{TupleAccessor, TypeAliasDef, resolve_type_alias_for_display, union_types};
1717

1818
use crate::{
1919
ast,
@@ -319,8 +319,14 @@ impl<'a> Compiler<'a> {
319319
ast::Type::Tuple(tuple) => {
320320
// Validate partial types have all named fields
321321
if tuple.is_partial {
322-
let has_unnamed = tuple.fields.iter().any(|f| f.name.is_none());
323-
let has_named = tuple.fields.iter().any(|f| f.name.is_some());
322+
let has_unnamed = tuple
323+
.fields
324+
.iter()
325+
.any(|f| matches!(f, ast::FieldType::Field { name: None, .. }));
326+
let has_named = tuple
327+
.fields
328+
.iter()
329+
.any(|f| matches!(f, ast::FieldType::Field { name: Some(_), .. }));
324330

325331
if has_unnamed && has_named {
326332
return Err(Error::TypeUnresolved(
@@ -330,7 +336,9 @@ impl<'a> Compiler<'a> {
330336
}
331337
// Recursively validate field types
332338
for field in &tuple.fields {
333-
Self::validate_type_ast(&field.type_def)?;
339+
if let ast::FieldType::Field { type_def, .. } = field {
340+
Self::validate_type_ast(type_def)?;
341+
}
334342
}
335343
Ok(())
336344
}
@@ -399,7 +407,7 @@ impl<'a> Compiler<'a> {
399407

400408
fn compile_tuple(
401409
&mut self,
402-
tuple_name: Option<String>,
410+
tuple_name: ast::TupleName,
403411
fields: Vec<ast::TupleField>,
404412
value_type: Option<Type>,
405413
) -> Result<Type, Error> {
@@ -409,57 +417,38 @@ impl<'a> Compiler<'a> {
409417
let contains_spread = Self::tuple_contains_spread(&fields);
410418

411419
if contains_spread {
412-
// Special handling for identifier[..., fields] and ~[..., fields] syntax:
413-
// If tuple name matches a spread identifier, use the source type's name
414-
let resolved_tuple_name = if let Some(ref name) = tuple_name {
415-
// Check if any spread field references this name
416-
let has_matching_spread = fields.iter().any(|f| {
417-
matches!(
418-
&f.value,
419-
ast::FieldValue::Spread(Some(id)) if id == name
420-
)
421-
});
422-
423-
if has_matching_spread {
424-
if name == "~" {
420+
// Resolve tuple name based on the TupleName variant
421+
let resolved_tuple_name = match tuple_name {
422+
ast::TupleName::Literal(name) => Some(name),
423+
ast::TupleName::None => None,
424+
ast::TupleName::Identifier(id) => {
425+
if id == "~" {
425426
// Ripple spread: ~[..., fields]
426427
// Get the ripple type from value_type (the piped value)
427-
if let Some(ref ripple_type) = value_type {
428+
value_type.as_ref().and_then(|ripple_type| {
428429
if let Type::Tuple(type_id) = ripple_type {
429-
if let Some(type_info) = self.program.lookup_type(type_id) {
430-
type_info.name.clone()
431-
} else {
432-
None
433-
}
430+
self.program
431+
.lookup_type(type_id)
432+
.and_then(|type_info| type_info.name.clone())
434433
} else {
435434
None
436435
}
437-
} else {
438-
None
439-
}
436+
})
440437
} else {
441438
// Identifier spread: identifier[..., fields]
442439
// Look up the variable's type
443-
if let Some((var_type, _)) = self.lookup_variable(&self.scopes, name, &[]) {
444-
// If it's a tuple type, use its name
445-
if let Type::Tuple(type_id) = var_type {
446-
if let Some(type_info) = self.program.lookup_type(&type_id) {
447-
type_info.name.clone()
440+
self.lookup_variable(&self.scopes, &id, &[])
441+
.and_then(|(var_type, _)| {
442+
if let Type::Tuple(type_id) = var_type {
443+
self.program
444+
.lookup_type(&type_id)
445+
.and_then(|type_info| type_info.name.clone())
448446
} else {
449-
tuple_name
447+
None
450448
}
451-
} else {
452-
tuple_name
453-
}
454-
} else {
455-
tuple_name
456-
}
449+
})
457450
}
458-
} else {
459-
tuple_name
460451
}
461-
} else {
462-
None
463452
};
464453

465454
// Use specialized compilation for tuples with spreads
@@ -507,7 +496,16 @@ impl<'a> Compiler<'a> {
507496
}
508497

509498
// Register the tuple type and emit instruction
510-
let type_id = self.program.register_type(tuple_name, field_types);
499+
let resolved_name = match tuple_name {
500+
ast::TupleName::Literal(name) => Some(name),
501+
ast::TupleName::None => None,
502+
ast::TupleName::Identifier(_) => {
503+
// Parser enforces that identifier syntax requires spreads,
504+
// so this path only executes when there are spreads (handled above)
505+
unreachable!("TupleName::Identifier without spreads should be rejected by parser")
506+
}
507+
};
508+
let type_id = self.program.register_type(resolved_name, field_types);
511509
self.codegen.add_instruction(Instruction::Tuple(type_id));
512510

513511
// If this tuple established a ripple context, clean up
@@ -647,7 +645,11 @@ impl<'a> Compiler<'a> {
647645

648646
if let Some(ast::Type::Tuple(tuple_type)) = &function.parameter_type {
649647
for field in &tuple_type.fields {
650-
if let Some(field_name) = &field.name {
648+
if let ast::FieldType::Field {
649+
name: Some(field_name),
650+
..
651+
} = field
652+
{
651653
function_params.insert(field_name.clone());
652654
}
653655
}
@@ -792,10 +794,14 @@ impl<'a> Compiler<'a> {
792794
let mut parameter_fields = HashMap::new();
793795
if let Some(ast::Type::Tuple(tuple_type)) = &function.parameter_type {
794796
for (field_index, field) in tuple_type.fields.iter().enumerate() {
795-
if let Some(field_name) = &field.name {
797+
if let ast::FieldType::Field {
798+
name: Some(field_name),
799+
type_def,
800+
} = field
801+
{
796802
let field_type = typing::resolve_ast_type(
797803
&self.type_aliases,
798-
field.type_def.clone(),
804+
type_def.clone(),
799805
&mut self.program,
800806
)?;
801807
parameter_fields.insert(field_name.clone(), (field_index, field_type));

0 commit comments

Comments
 (0)