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
37 changes: 27 additions & 10 deletions src/arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,13 @@ impl TypeArena {
/// Allocates a fresh copy of a type. For records, this clones the record definition.
pub fn alloc_type(&mut self, tpe: Type) -> Type {
if let Type::Record(rec) = tpe {
let key = Record(self.records.len());
let key = Record {
id: self.records.len(),
open: rec.open,
};
// TODO: technically, a deep-clone is needed here, where properties that point to
// records should also be allocated as well.
self.records.push(self.records[rec.0].clone());
self.records.push(self.records[rec.id].clone());

return Type::Record(key);
}
Expand All @@ -231,7 +234,21 @@ impl TypeArena {

/// Allocates a new record type from a map of field names to types.
pub fn alloc_record(&mut self, record: FxHashMap<StrRef, Type>) -> Record {
let key = Record(self.records.len());
let key = Record {
id: self.records.len(),
open: false,
};
self.records.push(record);
key
}

/// Allocates a new open record type from a map of field names to types. An open record type is
/// a record type that doesn't fail typechecking if we try to access a field that isn't defined yet.
pub fn alloc_open_record(&mut self, record: FxHashMap<StrRef, Type>) -> Record {
let key = Record {
id: self.records.len(),
open: true,
};
self.records.push(record);
key
}
Expand Down Expand Up @@ -259,7 +276,7 @@ impl TypeArena {

/// Returns the field map for the given record.
pub fn get_record(&self, key: Record) -> &FxHashMap<StrRef, Type> {
&self.records[key.0]
&self.records[key.id]
}

/// Returns the argument type slice for the given [`ArgsRef`].
Expand All @@ -279,7 +296,7 @@ impl TypeArena {

/// Returns the type of a field in the given record, or `None` if the field doesn't exist.
pub fn record_get(&self, record: Record, field: StrRef) -> Option<Type> {
self.records[record.0].get(&field).copied()
self.records[record.id].get(&field).copied()
}

/// Checks whether two records have the exact same set of field names.
Expand All @@ -304,19 +321,19 @@ impl TypeArena {
true
}

/// Creates an empty record type.
pub fn instantiate_record(&mut self) -> Record {
self.alloc_record(FxHashMap::default())
/// Creates an empty open record type.
pub fn instantiate_open_record(&mut self) -> Record {
self.alloc_open_record(FxHashMap::default())
}

/// Sets the type of a field in the given record, inserting or updating as needed.
pub fn record_set(&mut self, record: Record, field: StrRef, value: Type) {
self.records[record.0].insert(field, value);
self.records[record.id].insert(field, value);
}

/// Returns the number of fields in the given record.
pub fn record_len(&self, record: Record) -> usize {
self.records[record.0].len()
self.records[record.id].len()
}
}

Expand Down
25 changes: 25 additions & 0 deletions src/tests/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,28 @@ fn test_ids_in_group_by_should_pass() {
.map(|q| q.view(&session.arena))
}));
}

#[test]
fn test_project_event_decls() {
let mut builder = Session::builder().use_stdlib();

builder
.declare_type()
.define_record()
.prop("id", Type::String)
.prop("name", Type::String)
.prop("version", Type::String)
.prop("summary", Type::String)
.prop("schema", Type::Unspecified)
.for_data_source("command_decls");

let mut session = builder.build();

let query = session.parse(include_str!("./resources/project_event_decls.eql"));

insta::assert_yaml_snapshot!(query.and_then(|q| {
session
.run_static_analysis(q)
.map(|q| q.view(&session.arena))
}));
}
2 changes: 2 additions & 0 deletions src/tests/resources/project_event_decls.eql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM c IN command_decls
PROJECT INTO c.schema.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
source: src/tests/analysis.rs
expression: "query.and_then(|q|\n{ session.run_static_analysis(q).map(|q| q.view(&session.arena)) })"
---
Ok:
attrs:
pos:
line: 1
col: 1
sources:
- binding:
name: c
pos:
line: 1
col: 6
kind:
Name: command_decls
predicate: ~
group_by: ~
order_by: ~
limit: ~
projection:
attrs:
pos:
line: 2
col: 14
value:
Access:
target:
attrs:
pos:
line: 2
col: 14
value:
Access:
target:
attrs:
pos:
line: 2
col: 14
value:
Id: c
field: schema
field: properties
distinct: false
meta:
project: Unspecified
aggregate: false
40 changes: 10 additions & 30 deletions src/typing/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,16 +1136,13 @@ impl<'a> Analysis<'a> {
) -> AnalysisResult<Type> {
struct State {
depth: u8,
/// When true means we are into dynamically type object.
dynamic: bool,
definition: Def,
}

impl State {
fn new(definition: Def) -> Self {
Self {
depth: 0,
dynamic: false,
definition,
}
}
Expand Down Expand Up @@ -1183,7 +1180,7 @@ impl<'a> Analysis<'a> {
}
} else if let Some(tpe) = scope.get_mut(id) {
if matches!(tpe, Type::Unspecified) {
let record = arena.types.instantiate_record();
let record = arena.types.instantiate_open_record();
*tpe = Type::Record(record);

Ok(State::new(Def::User {
Expand Down Expand Up @@ -1217,23 +1214,12 @@ impl<'a> Analysis<'a> {
}
}
Value::Access(access) => {
let mut state = go(scope, arena, sys, access.target)?;

// TODO - we should consider make that field and depth configurable.
let is_data_field =
state.depth == 0 && arena.strings.get(access.field) == "data";

// TODO - we should consider make that behavior configurable.
// the `data` property is where the JSON payload is located, which means
// we should be lax if a property is not defined yet.
if !state.dynamic && is_data_field {
state.dynamic = true;
}
let state = go(scope, arena, sys, access.target)?;

match state.definition {
Def::User { parent, tpe } => {
if matches!(tpe, Type::Unspecified) && state.dynamic {
let record = arena.types.instantiate_record();
if matches!(tpe, Type::Unspecified) {
let record = arena.types.instantiate_open_record();
arena
.types
.record_set(record, access.field, Type::Unspecified);
Expand All @@ -1256,7 +1242,6 @@ impl<'a> Analysis<'a> {
},
tpe: Type::Unspecified,
},
..state
});
} else if let Type::Record(record) = tpe {
return if let Some(tpe) =
Expand All @@ -1271,11 +1256,9 @@ impl<'a> Analysis<'a> {
},
tpe,
},
..state
})
} else {
// TODO - that test seems useless because it can't be the data field and not be dynamic
if state.dynamic || is_data_field {
if record.open {
arena.types.record_set(
record,
access.field,
Expand All @@ -1290,7 +1273,6 @@ impl<'a> Analysis<'a> {
},
tpe: Type::Unspecified,
},
..state
});
}

Expand All @@ -1310,11 +1292,10 @@ impl<'a> Analysis<'a> {
}

Def::System(tpe) => {
if matches!(tpe, Type::Unspecified) && state.dynamic {
if matches!(tpe, Type::Unspecified) {
return Ok(State {
depth: state.depth + 1,
definition: Def::System(Type::Unspecified),
..state
});
}

Expand All @@ -1323,7 +1304,6 @@ impl<'a> Analysis<'a> {
return Ok(State {
depth: state.depth + 1,
definition: Def::System(field),
..state
});
}

Expand Down Expand Up @@ -1511,8 +1491,8 @@ impl Arena {
}

(Type::Record(a), Type::Record(b)) if self.types.records_have_same_keys(a, b) => {
let mut map_a = mem::take(&mut self.types.records[a.0]);
let mut map_b = mem::take(&mut self.types.records[b.0]);
let mut map_a = mem::take(&mut self.types.records[a.id]);
let mut map_b = mem::take(&mut self.types.records[b.id]);

for (bk, bv) in map_b.iter_mut() {
let av = map_a.get_mut(bk).unwrap();
Expand All @@ -1522,8 +1502,8 @@ impl Arena {
*bv = new_tpe;
}

self.types.records[a.0] = map_a;
self.types.records[b.0] = map_b;
self.types.records[a.id] = map_a;
self.types.records[b.id] = map_b;

Ok(Type::Record(a))
}
Expand Down
5 changes: 4 additions & 1 deletion src/typing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ pub struct TypeRef(pub(crate) usize);

/// A reference to a record definition stored in the [`TypeArena`](crate::arena::TypeArena).
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct Record(pub(crate) usize);
pub struct Record {
pub(crate) id: usize,
pub(crate) open: bool,
}

/// A reference to a function argument type list stored in the [`TypeArena`](crate::arena::TypeArena).
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
Expand Down
Loading