Skip to content

Commit 71b97a3

Browse files
authored
Merge pull request #25 from Virtual-Repetitions/better-join-tables
adding enum support (0.29.2)
2 parents 0dec8ba + b324b51 commit 71b97a3

28 files changed

Lines changed: 1270 additions & 140 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace.package]
2-
version = "0.29.0"
2+
version = "0.29.2"
33
edition = "2024"
44

55
# See https://github.com/mozilla/application-services/blob/main/Cargo.toml for the reasons why we use this structure

crates/builder/src/builder/interceptor_weaver.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ fn matches(expr: &AstExpr<Typed>, operation_name: &str, operation_kind: Operatio
8181
}
8282
},
8383
AstExpr::RelationalOp(_) => panic!("RelationalOp not supported in interceptor expression"),
84+
AstExpr::EnumLiteral(_, _, _, _) => {
85+
panic!("EnumLiteral not supported in interceptor expression")
86+
}
8487
AstExpr::StringLiteral(value, _) => matches_str(value, operation_name, operation_kind),
8588
AstExpr::BooleanLiteral(value, _) => *value,
8689
AstExpr::NumberLiteral(_, _) => {

crates/builder/src/typechecker/expression.rs

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
use std::collections::HashMap;
1111

12-
use codemap_diagnostic::Diagnostic;
12+
use codemap_diagnostic::{Diagnostic, Level, SpanLabel, SpanStyle};
1313
use core_model::mapped_arena::MappedArena;
1414
use core_model_builder::typechecker::{Typed, annotation::AnnotationSpec};
1515

16-
use crate::ast::ast_types::{AstExpr, FieldSelection, LogicalOp, RelationalOp, Untyped};
16+
use crate::ast::ast_types::{
17+
AstExpr, FieldSelection, FieldSelectionElement, LogicalOp, RelationalOp, Untyped,
18+
};
1719

1820
use super::{Scope, Type, TypecheckFrom};
1921

@@ -27,6 +29,9 @@ impl TypecheckFrom<AstExpr<Untyped>> for AstExpr<Typed> {
2729
AstExpr::RelationalOp(relation) => {
2830
AstExpr::RelationalOp(RelationalOp::shallow(relation))
2931
}
32+
AstExpr::EnumLiteral(enum_name, value, span, _) => {
33+
AstExpr::EnumLiteral(enum_name.clone(), value.clone(), *span, Type::Defer)
34+
}
3035
AstExpr::StringLiteral(v, s) => AstExpr::StringLiteral(v.clone(), *s),
3136
AstExpr::BooleanLiteral(v, s) => AstExpr::BooleanLiteral(*v, *s),
3237
AstExpr::NumberLiteral(v, s) => AstExpr::NumberLiteral(v.clone(), *s),
@@ -49,11 +54,39 @@ impl TypecheckFrom<AstExpr<Untyped>> for AstExpr<Typed> {
4954
errors: &mut Vec<Diagnostic>,
5055
) -> bool {
5156
match self {
52-
AstExpr::FieldSelection(select) => select.pass(type_env, annotation_env, scope, errors),
57+
AstExpr::FieldSelection(select) => {
58+
if let Some((enum_name, value, span)) = enum_literal_parts(select) {
59+
if let Some(typ) =
60+
resolve_enum_literal(&enum_name, &value, span, type_env, scope, errors)
61+
{
62+
*self = AstExpr::EnumLiteral(enum_name, value, span, typ);
63+
true
64+
} else {
65+
select.pass(type_env, annotation_env, scope, errors)
66+
}
67+
} else {
68+
select.pass(type_env, annotation_env, scope, errors)
69+
}
70+
}
5371
AstExpr::LogicalOp(logic) => logic.pass(type_env, annotation_env, scope, errors),
5472
AstExpr::RelationalOp(relation) => {
5573
relation.pass(type_env, annotation_env, scope, errors)
5674
}
75+
AstExpr::EnumLiteral(enum_name, value, span, typ) => {
76+
if typ.is_incomplete() {
77+
if let Some(resolved) =
78+
resolve_enum_literal(enum_name, value, *span, type_env, scope, errors)
79+
{
80+
*typ = resolved;
81+
true
82+
} else {
83+
*typ = Type::Error;
84+
false
85+
}
86+
} else {
87+
false
88+
}
89+
}
5790
AstExpr::StringList(_, _)
5891
| AstExpr::StringLiteral(_, _)
5992
| AstExpr::BooleanLiteral(_, _)
@@ -63,3 +96,76 @@ impl TypecheckFrom<AstExpr<Untyped>> for AstExpr<Typed> {
6396
}
6497
}
6598
}
99+
100+
fn enum_literal_parts(
101+
selection: &FieldSelection<Typed>,
102+
) -> Option<(String, String, codemap::Span)> {
103+
match selection {
104+
FieldSelection::Select(prefix, elem, span, _) => match (prefix.as_ref(), elem) {
105+
(
106+
FieldSelection::Single(prefix_elem, _),
107+
FieldSelectionElement::Identifier(value, _, _),
108+
) => {
109+
if let FieldSelectionElement::Identifier(enum_name, _, _) = prefix_elem {
110+
Some((enum_name.clone(), value.clone(), *span))
111+
} else {
112+
None
113+
}
114+
}
115+
_ => None,
116+
},
117+
_ => None,
118+
}
119+
}
120+
121+
fn resolve_enum_literal(
122+
enum_name: &str,
123+
value: &str,
124+
span: codemap::Span,
125+
type_env: &MappedArena<Type>,
126+
scope: &Scope,
127+
errors: &mut Vec<Diagnostic>,
128+
) -> Option<Type> {
129+
if scope.get_type(enum_name).is_some() {
130+
return None;
131+
}
132+
133+
let is_context = type_env
134+
.get_by_key(enum_name)
135+
.and_then(|t| match t {
136+
Type::Composite(c) if c.kind == crate::ast::ast_types::AstModelKind::Context => Some(c),
137+
_ => None,
138+
})
139+
.is_some();
140+
141+
if is_context {
142+
return None;
143+
}
144+
145+
let enum_type = type_env.get_by_key(enum_name).and_then(|t| match t {
146+
Type::Enum(e) => Some(e.clone()),
147+
_ => None,
148+
});
149+
150+
#[allow(clippy::question_mark)]
151+
let enum_type = match enum_type {
152+
Some(enum_type) => enum_type,
153+
None => return None,
154+
};
155+
156+
if enum_type.fields.iter().any(|f| f.name == value) {
157+
Some(Type::Enum(enum_type))
158+
} else {
159+
errors.push(Diagnostic {
160+
level: Level::Error,
161+
message: format!("Unknown variant '{value}' for enum '{enum_name}'"),
162+
code: Some("C000".to_string()),
163+
spans: vec![SpanLabel {
164+
span,
165+
style: SpanStyle::Primary,
166+
label: Some("unknown enum variant".to_string()),
167+
}],
168+
});
169+
Some(Type::Error)
170+
}
171+
}

crates/builder/src/typechecker/field.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl TypecheckFrom<AstField<Untyped>> for AstField<Typed> {
8484
}
8585
};
8686

87-
match *expr {
87+
match expr {
8888
AstExpr::StringLiteral(_, _) => assert_type(&[
8989
"String",
9090
"Decimal",
@@ -97,6 +97,22 @@ impl TypecheckFrom<AstField<Untyped>> for AstField<Typed> {
9797
]),
9898
AstExpr::BooleanLiteral(_, _) => assert_type(&["Boolean"]),
9999
AstExpr::NumberLiteral(_, _) => assert_type(&["Int", "Float"]),
100+
AstExpr::EnumLiteral(enum_name, _, _, _) => {
101+
if enum_name != &type_name {
102+
errors.push(Diagnostic {
103+
level: Level::Error,
104+
message:
105+
"Literal specified for default value is not a valid type for field."
106+
.to_string(),
107+
code: Some("C000".to_string()),
108+
spans: vec![SpanLabel {
109+
span: expr.span(),
110+
style: SpanStyle::Primary,
111+
label: Some(format!("should be of type {type_name}")),
112+
}],
113+
});
114+
}
115+
}
100116
AstExpr::FieldSelection(_) => {
101117
// no type-checking here, since we don't have enough information.
102118
// For example `user: User = AuthContext.id` should check that `AuthContext.id`

crates/builder/src/typechecker/field_default_value.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ impl TypecheckFrom<AstFieldDefault<Untyped>> for AstFieldDefault<Typed> {
4949
AstExpr::BooleanLiteral(_, _)
5050
| AstExpr::StringLiteral(_, _)
5151
| AstExpr::NumberLiteral(_, _)
52+
| AstExpr::EnumLiteral(_, _, _, _)
5253
| AstExpr::FieldSelection(_) => expr.pass(type_env, annotation_env, scope, errors),
5354

5455
_ => {

crates/builder/src/typechecker/mod.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ pub mod test_support {
494494
#[cfg(test)]
495495
mod tests {
496496
use super::test_support::{build, parse_sorted};
497+
use core_model_builder::ast::ast_types::{AstAnnotationParams, AstExpr};
497498
use multiplatform_test::multiplatform_test;
498499

499500
// Due to a change in insta version 1.12, test names (hence the snapshot names) get derived
@@ -561,6 +562,57 @@ mod tests {
561562
assert_typechecking!(src, "with_auth_context_use_in_field_annotation");
562563
}
563564

565+
#[multiplatform_test]
566+
fn enum_literal_in_access_predicate() {
567+
let src = r#"
568+
@postgres
569+
module TagModule {
570+
enum TagSharingType {
571+
PublicPlaybook
572+
PrivatePlaybook
573+
}
574+
575+
type Tag {
576+
sharing: TagSharingType
577+
@access(query=self.sharing == TagSharingType.PublicPlaybook) title: String
578+
}
579+
}
580+
"#;
581+
582+
let built = build(src).unwrap();
583+
let module = built
584+
.modules
585+
.iter()
586+
.find(|(_, module)| module.0.name == "TagModule")
587+
.map(|(_, module)| module)
588+
.unwrap();
589+
let tag_type = module.0.types.iter().find(|t| t.name == "Tag").unwrap();
590+
let title_field = tag_type.fields.iter().find(|f| f.name == "title").unwrap();
591+
let access = title_field.annotations.annotations.get("access").unwrap();
592+
593+
let expr = match &access.params {
594+
AstAnnotationParams::Map(params, _) => params
595+
.iter()
596+
.find(|(k, _)| k.as_str() == "query")
597+
.map(|(_, v)| v)
598+
.unwrap(),
599+
_ => panic!("Expected map access expression"),
600+
};
601+
602+
let AstExpr::RelationalOp(rel) = expr else {
603+
panic!("Expected relational op in access expression");
604+
};
605+
606+
let (_, right) = rel.sides();
607+
match right {
608+
AstExpr::EnumLiteral(enum_name, value, _, _) => {
609+
assert_eq!(enum_name, "TagSharingType");
610+
assert_eq!(value, "PublicPlaybook");
611+
}
612+
_ => panic!("Expected enum literal on right-hand side"),
613+
}
614+
}
615+
564616
#[multiplatform_test]
565617
fn with_array_in_operator() {
566618
let src = r#"

crates/core-subsystem/core-model-builder/src/ast/ast_types.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,15 @@ pub enum AstExpr<T: NodeTypedness> {
316316
FieldSelection(FieldSelection<T>),
317317
LogicalOp(LogicalOp<T>),
318318
RelationalOp(RelationalOp<T>),
319+
EnumLiteral(
320+
String,
321+
String,
322+
#[serde(skip_serializing)]
323+
#[serde(skip_deserializing)]
324+
#[serde(default = "default_span")]
325+
Span,
326+
T::Expr,
327+
),
319328
StringLiteral(
320329
String,
321330
#[serde(skip_serializing)]
@@ -362,6 +371,7 @@ impl<T: NodeTypedness> AstExpr<T> {
362371
pub fn span(&self) -> Span {
363372
match &self {
364373
AstExpr::FieldSelection(s) => *s.span(),
374+
AstExpr::EnumLiteral(_, _, s, _) => *s,
365375
AstExpr::StringLiteral(_, s) => *s,
366376
AstExpr::LogicalOp(l) => match l {
367377
LogicalOp::Not(_, s, _) => *s,

crates/core-subsystem/core-model-builder/src/typechecker/expression.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ impl AstExpr<Typed> {
1919
AstExpr::FieldSelection(select) => select.typ().clone(),
2020
AstExpr::LogicalOp(logic) => logic.typ().clone(),
2121
AstExpr::RelationalOp(relation) => relation.typ().clone(),
22+
AstExpr::EnumLiteral(_, _, _, typ) => typ.clone(),
2223
AstExpr::StringLiteral(_, _) => {
2324
Type::Primitive(PrimitiveType::Plain(primitive_type::STRING_TYPE))
2425
}

crates/core-subsystem/core-model/src/access.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ pub enum CommonAccessPrimitiveExpression {
135135
StringLiteral(String), // for example, "ADMIN"
136136
BooleanLiteral(bool), // for example, true
137137
NumberLiteral(String), // for example, integer (-13, 0, 300, 10.5, etc.)
138+
EnumLiteral { enum_name: String, value: String },
138139
NullLiteral,
139140
}
140141

0 commit comments

Comments
 (0)