2017-01-04 13:09:12 +00:00
|
|
|
// Copyright 2016 Mozilla
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
|
|
// this file except in compliance with the License. You may obtain a copy of the
|
|
|
|
// License at http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
// Unless required by applicable law or agreed to in writing, software distributed
|
|
|
|
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
|
|
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
|
|
// specific language governing permissions and limitations under the License.
|
|
|
|
|
2017-01-25 22:06:19 +00:00
|
|
|
///! This module defines some core types that support find expressions: sources,
|
|
|
|
///! variables, expressions, etc.
|
|
|
|
///! These are produced as 'fuel' by the query parser, consumed by the query
|
|
|
|
///! translator and executor.
|
|
|
|
///!
|
|
|
|
///! Many of these types are defined as simple structs that are little more than
|
|
|
|
///! a richer type alias: a variable, for example, is really just a fancy kind
|
|
|
|
///! of string.
|
|
|
|
///!
|
|
|
|
///! At some point in the future, we might consider reducing copying and memory
|
|
|
|
///! usage by recasting all of these string-holding structs and enums in terms
|
|
|
|
///! of string references, with those references being slices of some parsed
|
|
|
|
///! input query string, and valid for the lifetime of that string.
|
|
|
|
///!
|
|
|
|
///! For now, for the sake of simplicity, all of these strings are heap-allocated.
|
|
|
|
///!
|
|
|
|
///! Furthermore, we might cut out some of the chaff here: each time a 'tagged'
|
|
|
|
///! type is used within an enum, we have an opportunity to simplify and use the
|
|
|
|
///! inner type directly in conjunction with matching on the enum. Before diving
|
|
|
|
///! deeply into this it's worth recognizing that this loss of 'sovereignty' is
|
|
|
|
///! a tradeoff against well-typed function signatures and other such boundaries.
|
|
|
|
|
|
|
|
extern crate edn;
|
2017-02-10 00:59:35 +00:00
|
|
|
extern crate mentat_core;
|
2017-01-25 22:06:19 +00:00
|
|
|
|
2017-02-04 00:51:57 +00:00
|
|
|
use edn::{BigInt, OrderedFloat};
|
|
|
|
pub use edn::{NamespacedKeyword, PlainSymbol};
|
2017-02-10 00:59:35 +00:00
|
|
|
use mentat_core::TypedValue;
|
2017-01-25 22:06:19 +00:00
|
|
|
|
|
|
|
pub type SrcVarName = String; // Do not include the required syntactic '$'.
|
|
|
|
|
2017-02-08 20:50:35 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
2017-01-25 22:06:19 +00:00
|
|
|
pub struct Variable(pub PlainSymbol);
|
|
|
|
|
2017-02-01 22:51:02 +00:00
|
|
|
pub trait FromValue<T> {
|
|
|
|
fn from_value(v: &edn::Value) -> Option<T>;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// If the provided EDN value is a PlainSymbol beginning with '?', return
|
|
|
|
/// it wrapped in a Variable. If not, return None.
|
|
|
|
impl FromValue<Variable> for Variable {
|
|
|
|
fn from_value(v: &edn::Value) -> Option<Variable> {
|
|
|
|
if let edn::Value::PlainSymbol(ref s) = *v {
|
|
|
|
Variable::from_symbol(s)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Variable {
|
2017-02-03 02:32:00 +00:00
|
|
|
pub fn from_symbol(sym: &PlainSymbol) -> Option<Variable> {
|
2017-02-01 22:51:02 +00:00
|
|
|
if sym.is_var_symbol() {
|
|
|
|
Some(Variable(sym.clone()))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2017-01-25 22:06:19 +00:00
|
|
|
pub enum SrcVar {
|
|
|
|
DefaultSrc,
|
|
|
|
NamedSrc(SrcVarName),
|
|
|
|
}
|
|
|
|
|
2017-02-01 22:51:02 +00:00
|
|
|
impl FromValue<SrcVar> for SrcVar {
|
|
|
|
fn from_value(v: &edn::Value) -> Option<SrcVar> {
|
|
|
|
if let edn::Value::PlainSymbol(ref s) = *v {
|
|
|
|
SrcVar::from_symbol(s)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SrcVar {
|
|
|
|
pub fn from_symbol(sym: &PlainSymbol) -> Option<SrcVar> {
|
|
|
|
if sym.is_src_symbol() {
|
|
|
|
Some(SrcVar::NamedSrc(sym.plain_name().to_string()))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-25 22:06:19 +00:00
|
|
|
/// These are the scalar values representable in EDN.
|
2017-02-03 02:32:00 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2017-01-25 22:06:19 +00:00
|
|
|
pub enum NonIntegerConstant {
|
|
|
|
Boolean(bool),
|
|
|
|
BigInteger(BigInt),
|
|
|
|
Float(OrderedFloat<f64>),
|
|
|
|
Text(String),
|
|
|
|
}
|
|
|
|
|
2017-02-10 00:59:35 +00:00
|
|
|
impl NonIntegerConstant {
|
|
|
|
pub fn into_typed_value(self) -> TypedValue {
|
|
|
|
match self {
|
|
|
|
NonIntegerConstant::BigInteger(_) => unimplemented!(), // TODO: #280.
|
|
|
|
NonIntegerConstant::Boolean(v) => TypedValue::Boolean(v),
|
|
|
|
NonIntegerConstant::Float(v) => TypedValue::Double(v),
|
|
|
|
NonIntegerConstant::Text(v) => TypedValue::String(v),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-25 22:06:19 +00:00
|
|
|
pub enum FnArg {
|
|
|
|
Variable(Variable),
|
|
|
|
SrcVar(SrcVar),
|
|
|
|
EntidOrInteger(i64),
|
|
|
|
Ident(NamespacedKeyword),
|
|
|
|
Constant(NonIntegerConstant),
|
|
|
|
}
|
|
|
|
|
|
|
|
/// e, a, tx can't be values -- no strings, no floats -- and so
|
|
|
|
/// they can only be variables, entity IDs, ident keywords, or
|
|
|
|
/// placeholders.
|
|
|
|
/// This encoding allows us to represent integers that aren't
|
|
|
|
/// entity IDs. That'll get filtered out in the context of the
|
|
|
|
/// database.
|
2017-02-03 02:32:00 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2017-01-25 22:06:19 +00:00
|
|
|
pub enum PatternNonValuePlace {
|
|
|
|
Placeholder,
|
|
|
|
Variable(Variable),
|
2017-02-03 02:32:00 +00:00
|
|
|
Entid(i64), // Will always be +ve. See #190.
|
2017-01-25 22:06:19 +00:00
|
|
|
Ident(NamespacedKeyword),
|
|
|
|
}
|
|
|
|
|
2017-02-03 02:32:00 +00:00
|
|
|
impl PatternNonValuePlace {
|
|
|
|
// I think we'll want move variants, so let's leave these here for now.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
fn into_pattern_value_place(self) -> PatternValuePlace {
|
|
|
|
match self {
|
|
|
|
PatternNonValuePlace::Placeholder => PatternValuePlace::Placeholder,
|
|
|
|
PatternNonValuePlace::Variable(x) => PatternValuePlace::Variable(x),
|
|
|
|
PatternNonValuePlace::Entid(x) => PatternValuePlace::EntidOrInteger(x),
|
|
|
|
PatternNonValuePlace::Ident(x) => PatternValuePlace::IdentOrKeyword(x),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_pattern_value_place(&self) -> PatternValuePlace {
|
|
|
|
match *self {
|
|
|
|
PatternNonValuePlace::Placeholder => PatternValuePlace::Placeholder,
|
|
|
|
PatternNonValuePlace::Variable(ref x) => PatternValuePlace::Variable(x.clone()),
|
|
|
|
PatternNonValuePlace::Entid(x) => PatternValuePlace::EntidOrInteger(x),
|
|
|
|
PatternNonValuePlace::Ident(ref x) => PatternValuePlace::IdentOrKeyword(x.clone()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromValue<PatternNonValuePlace> for PatternNonValuePlace {
|
|
|
|
fn from_value(v: &edn::Value) -> Option<PatternNonValuePlace> {
|
|
|
|
match v {
|
|
|
|
&edn::Value::Integer(x) => if x >= 0 {
|
|
|
|
Some(PatternNonValuePlace::Entid(x))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
&edn::Value::PlainSymbol(ref x) => if x.0.as_str() == "_" {
|
|
|
|
Some(PatternNonValuePlace::Placeholder)
|
|
|
|
} else {
|
|
|
|
if let Some(v) = Variable::from_symbol(x) {
|
|
|
|
Some(PatternNonValuePlace::Variable(v))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
},
|
|
|
|
&edn::Value::NamespacedKeyword(ref x) =>
|
|
|
|
Some(PatternNonValuePlace::Ident(x.clone())),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-04 00:52:03 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum IdentOrEntid {
|
|
|
|
Ident(NamespacedKeyword),
|
|
|
|
Entid(i64),
|
|
|
|
}
|
|
|
|
|
2017-01-25 22:06:19 +00:00
|
|
|
/// The `v` part of a pattern can be much broader: it can represent
|
|
|
|
/// integers that aren't entity IDs (particularly negative integers),
|
|
|
|
/// strings, and all the rest. We group those under `Constant`.
|
2017-02-03 02:32:00 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2017-01-25 22:06:19 +00:00
|
|
|
pub enum PatternValuePlace {
|
|
|
|
Placeholder,
|
|
|
|
Variable(Variable),
|
|
|
|
EntidOrInteger(i64),
|
2017-02-03 02:32:00 +00:00
|
|
|
IdentOrKeyword(NamespacedKeyword),
|
2017-01-25 22:06:19 +00:00
|
|
|
Constant(NonIntegerConstant),
|
|
|
|
}
|
|
|
|
|
2017-02-03 02:32:00 +00:00
|
|
|
impl FromValue<PatternValuePlace> for PatternValuePlace {
|
|
|
|
fn from_value(v: &edn::Value) -> Option<PatternValuePlace> {
|
|
|
|
match v {
|
|
|
|
&edn::Value::Integer(x) =>
|
|
|
|
Some(PatternValuePlace::EntidOrInteger(x)),
|
|
|
|
&edn::Value::PlainSymbol(ref x) if x.0.as_str() == "_" =>
|
|
|
|
Some(PatternValuePlace::Placeholder),
|
|
|
|
&edn::Value::PlainSymbol(ref x) if x.is_var_symbol() =>
|
|
|
|
Some(PatternValuePlace::Variable(Variable(x.clone()))),
|
|
|
|
&edn::Value::NamespacedKeyword(ref x) =>
|
|
|
|
Some(PatternValuePlace::IdentOrKeyword(x.clone())),
|
|
|
|
&edn::Value::Boolean(x) =>
|
|
|
|
Some(PatternValuePlace::Constant(NonIntegerConstant::Boolean(x))),
|
|
|
|
&edn::Value::Float(x) =>
|
|
|
|
Some(PatternValuePlace::Constant(NonIntegerConstant::Float(x))),
|
|
|
|
&edn::Value::BigInteger(ref x) =>
|
|
|
|
Some(PatternValuePlace::Constant(NonIntegerConstant::BigInteger(x.clone()))),
|
|
|
|
&edn::Value::Text(ref x) =>
|
|
|
|
Some(PatternValuePlace::Constant(NonIntegerConstant::Text(x.clone()))),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PatternValuePlace {
|
|
|
|
// I think we'll want move variants, so let's leave these here for now.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
fn into_pattern_non_value_place(self) -> Option<PatternNonValuePlace> {
|
|
|
|
match self {
|
|
|
|
PatternValuePlace::Placeholder => Some(PatternNonValuePlace::Placeholder),
|
|
|
|
PatternValuePlace::Variable(x) => Some(PatternNonValuePlace::Variable(x)),
|
|
|
|
PatternValuePlace::EntidOrInteger(x) => if x >= 0 {
|
|
|
|
Some(PatternNonValuePlace::Entid(x))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
PatternValuePlace::IdentOrKeyword(x) => Some(PatternNonValuePlace::Ident(x)),
|
|
|
|
PatternValuePlace::Constant(_) => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_pattern_non_value_place(&self) -> Option<PatternNonValuePlace> {
|
|
|
|
match *self {
|
|
|
|
PatternValuePlace::Placeholder => Some(PatternNonValuePlace::Placeholder),
|
|
|
|
PatternValuePlace::Variable(ref x) => Some(PatternNonValuePlace::Variable(x.clone())),
|
|
|
|
PatternValuePlace::EntidOrInteger(x) => if x >= 0 {
|
|
|
|
Some(PatternNonValuePlace::Entid(x))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
PatternValuePlace::IdentOrKeyword(ref x) => Some(PatternNonValuePlace::Ident(x.clone())),
|
|
|
|
PatternValuePlace::Constant(_) => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-25 22:06:19 +00:00
|
|
|
/*
|
|
|
|
pub enum PullPattern {
|
|
|
|
Constant(Constant),
|
|
|
|
Variable(Variable),
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Pull {
|
|
|
|
pub src: SrcVar,
|
|
|
|
pub var: Variable,
|
|
|
|
pub pattern: PullPattern, // Constant, variable, or plain variable.
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
pub struct Aggregate {
|
|
|
|
pub fn_name: String,
|
|
|
|
pub args: Vec<FnArg>,
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
#[derive(Clone,Debug,Eq,PartialEq)]
|
|
|
|
pub enum Element {
|
|
|
|
Variable(Variable),
|
|
|
|
// Aggregate(Aggregate), // TODO
|
|
|
|
// Pull(Pull), // TODO
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A definition of the first part of a find query: the
|
|
|
|
/// `[:find ?foo ?bar…]` bit.
|
|
|
|
///
|
|
|
|
/// There are four different kinds of find specs, allowing you to query for
|
|
|
|
/// a single value, a collection of values from different entities, a single
|
|
|
|
/// tuple (relation), or a collection of tuples.
|
|
|
|
///
|
|
|
|
/// Examples:
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// # extern crate edn;
|
|
|
|
/// # extern crate mentat_query;
|
|
|
|
/// # use edn::PlainSymbol;
|
|
|
|
/// # use mentat_query::{Element, FindSpec, Variable};
|
|
|
|
///
|
|
|
|
/// # fn main() {
|
|
|
|
///
|
|
|
|
/// let elements = vec![
|
|
|
|
/// Element::Variable(Variable(PlainSymbol("?foo".to_string()))),
|
|
|
|
/// Element::Variable(Variable(PlainSymbol("?bar".to_string()))),
|
|
|
|
/// ];
|
|
|
|
/// let rel = FindSpec::FindRel(elements);
|
|
|
|
///
|
|
|
|
/// if let FindSpec::FindRel(elements) = rel {
|
|
|
|
/// assert_eq!(2, elements.len());
|
|
|
|
/// }
|
|
|
|
///
|
|
|
|
/// # }
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
#[derive(Clone,Debug,Eq,PartialEq)]
|
|
|
|
pub enum FindSpec {
|
|
|
|
/// Returns an array of arrays.
|
|
|
|
FindRel(Vec<Element>),
|
|
|
|
|
|
|
|
/// Returns an array of scalars, usually homogeneous.
|
|
|
|
/// This is equivalent to mapping over the results of a `FindRel`,
|
|
|
|
/// returning the first value of each.
|
|
|
|
FindColl(Element),
|
|
|
|
|
|
|
|
/// Returns a single tuple: a heterogeneous array of scalars. Equivalent to
|
|
|
|
/// taking the first result from a `FindRel`.
|
|
|
|
FindTuple(Vec<Element>),
|
|
|
|
|
|
|
|
/// Returns a single scalar value. Equivalent to taking the first result
|
|
|
|
/// from a `FindColl`.
|
|
|
|
FindScalar(Element),
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if the provided `FindSpec` returns at most one result.
|
|
|
|
pub fn is_unit_limited(spec: &FindSpec) -> bool {
|
|
|
|
match spec {
|
|
|
|
&FindSpec::FindScalar(..) => true,
|
|
|
|
&FindSpec::FindTuple(..) => true,
|
|
|
|
&FindSpec::FindRel(..) => false,
|
|
|
|
&FindSpec::FindColl(..) => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if the provided `FindSpec` cares about distinct results.
|
|
|
|
///
|
|
|
|
/// I use the words "cares about" because find is generally defined in terms of producing distinct
|
|
|
|
/// results at the Datalog level.
|
|
|
|
///
|
|
|
|
/// Two of the find specs (scalar and tuple) produce only a single result. Those don't need to be
|
|
|
|
/// run with `SELECT DISTINCT`, because we're only consuming a single result. Those queries will be
|
|
|
|
/// run with `LIMIT 1`.
|
|
|
|
///
|
|
|
|
/// Additionally, some projections cannot produce duplicate results: `[:find (max ?x) …]`, for
|
|
|
|
/// example.
|
|
|
|
///
|
|
|
|
/// This function gives us the hook to add that logic when we're ready.
|
|
|
|
///
|
|
|
|
/// Beyond this, `DISTINCT` is not always needed. For example, in some kinds of accumulation or
|
|
|
|
/// sampling projections we might not need to do it at the SQL level because we're consuming into
|
|
|
|
/// a dupe-eliminating data structure like a Set, or we know that a particular query cannot produce
|
|
|
|
/// duplicate results.
|
|
|
|
pub fn requires_distinct(spec: &FindSpec) -> bool {
|
|
|
|
return !is_unit_limited(spec);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note that the "implicit blank" rule applies.
|
|
|
|
// A pattern with a reversed attribute — :foo/_bar — is reversed
|
|
|
|
// at the point of parsing. These `Pattern` instances only represent
|
|
|
|
// one direction.
|
2017-02-03 02:32:00 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2017-01-25 22:06:19 +00:00
|
|
|
pub struct Pattern {
|
2017-02-03 02:32:00 +00:00
|
|
|
pub source: Option<SrcVar>,
|
|
|
|
pub entity: PatternNonValuePlace,
|
|
|
|
pub attribute: PatternNonValuePlace,
|
|
|
|
pub value: PatternValuePlace,
|
|
|
|
pub tx: PatternNonValuePlace,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Pattern {
|
|
|
|
pub fn new(src: Option<SrcVar>,
|
|
|
|
e: PatternNonValuePlace,
|
|
|
|
a: PatternNonValuePlace,
|
|
|
|
v: PatternValuePlace,
|
|
|
|
tx: PatternNonValuePlace) -> Option<Pattern> {
|
|
|
|
let aa = a.clone(); // Too tired of fighting borrow scope for now.
|
|
|
|
if let PatternNonValuePlace::Ident(ref k) = aa {
|
|
|
|
if k.is_backward() {
|
|
|
|
// e and v have different types; we must convert them.
|
|
|
|
// Not every parseable value is suitable for the entity field!
|
|
|
|
// As such, this is a failable constructor.
|
|
|
|
let e_v = e.to_pattern_value_place();
|
|
|
|
if let Some(v_e) = v.to_pattern_non_value_place() {
|
|
|
|
return Some(Pattern {
|
|
|
|
source: src,
|
|
|
|
entity: v_e,
|
|
|
|
attribute: PatternNonValuePlace::Ident(k.to_reversed()),
|
|
|
|
value: e_v,
|
|
|
|
tx: tx,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(Pattern {
|
|
|
|
source: src,
|
|
|
|
entity: e,
|
|
|
|
attribute: a,
|
|
|
|
value: v,
|
|
|
|
tx: tx,
|
|
|
|
})
|
|
|
|
}
|
2017-01-25 22:06:19 +00:00
|
|
|
}
|
|
|
|
|
2017-02-03 02:32:00 +00:00
|
|
|
|
2017-01-25 22:06:19 +00:00
|
|
|
#[allow(dead_code)]
|
2017-02-03 02:32:00 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2017-01-25 22:06:19 +00:00
|
|
|
pub enum WhereClause {
|
|
|
|
Not,
|
|
|
|
NotJoin,
|
|
|
|
Or,
|
|
|
|
OrJoin,
|
|
|
|
Pred,
|
|
|
|
WhereFn,
|
|
|
|
RuleExpr,
|
2017-02-03 02:32:00 +00:00
|
|
|
Pattern(Pattern),
|
2017-01-25 22:06:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
2017-02-03 02:32:00 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub struct FindQuery {
|
|
|
|
pub find_spec: FindSpec,
|
|
|
|
pub default_source: SrcVar,
|
|
|
|
pub with: Vec<Variable>,
|
|
|
|
pub in_vars: Vec<Variable>,
|
|
|
|
pub in_sources: Vec<SrcVar>,
|
|
|
|
pub where_clauses: Vec<WhereClause>,
|
2017-01-25 22:06:19 +00:00
|
|
|
// TODO: in_rules;
|
|
|
|
}
|