Finish type resolution for simple expressions and columns

This commit is contained in:
Simon Binder 2019-06-28 10:50:41 +02:00
parent 7e916b9d74
commit 1271e730b8
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 165 additions and 186 deletions

View File

@ -16,7 +16,6 @@ part 'steps/type_resolver.dart';
part 'types/data.dart';
part 'types/resolver.dart';
part 'types/typeable.dart';
part 'error.dart';

View File

@ -11,4 +11,14 @@ class AnalysisContext {
void reportError(AnalysisError error) {
errors.add(error);
}
ResolveResult typeOf(Typeable t) {
if (t is Column) {
return types.resolveColumn(t);
} else if (t is Expression) {
return types.resolveExpression(t);
}
throw StateError('Unknown typeable $t');
}
}

View File

@ -2,13 +2,16 @@ part of '../analysis.dart';
abstract class Column with Referencable implements Typeable {
String get name;
const Column();
}
class TableColumn extends Column {
@override
final String name;
final ResolvedType type;
TableColumn(this.name);
const TableColumn(this.name, this.type);
}
class ExpressionColumn extends Column {

View File

@ -1,16 +1,5 @@
part of '../analysis.dart';
const _comparisons = [
TokenType.less,
TokenType.lessEqual,
TokenType.more,
TokenType.moreEqual,
TokenType.equal,
TokenType.doubleEqual,
TokenType.exclamationEqual,
TokenType.lessMore,
];
class TypeResolvingVisitor extends RecursiveVisitor<void> {
final AnalysisContext context;
TypeResolver get types => context.types;
@ -18,99 +7,14 @@ class TypeResolvingVisitor extends RecursiveVisitor<void> {
TypeResolvingVisitor(this.context);
@override
void visitSelectStatement(SelectStatement e) {
if (e.where != null) {
context.types.suggestBool(e.where);
void visitChildren(AstNode e) {
// called for every ast node, so we implement this here
if (e is Expression) {
types.resolveExpression(e);
} else if (e is SelectStatement) {
e.resolvedColumns.forEach(types.resolveColumn);
}
visitChildren(e);
}
@override
void visitResultColumn(ResultColumn e) {
visitChildren(e);
}
@override
void visitFunction(FunctionExpression e) {
// todo handle function calls
visitChildren(e);
}
@override
void visitLimit(Limit e) {
if (e.count != null) {
types.suggestType(e.count, const SqlType.int());
}
if (e.offset != null) {
types.suggestType(e.offset, const SqlType.int());
}
visitChildren(e);
}
@override
void visitBinaryExpression(BinaryExpression e) {
final operator = e.operator.type;
if (operator == TokenType.doublePipe) {
// string concatenation: Will return a string, makes most sense with a
// string.
types
..forceType(e, const SqlType.text())
..suggestType(e.left, const SqlType.text())
..suggestType(e.right, const SqlType.text());
} else if (operator == TokenType.and || operator == TokenType.or) {
types
..suggestBool(e.left)
..suggestBool(e.right)
..forceType(e, const SqlType.int())
..addTypeHint(e, const IsBoolean());
} else if (_comparisons.contains(operator)) {
types
..suggestBool(e)
..suggestSame(e.left, e.right);
} else {
// arithmetic operator
types
..forceType(e, const AnyNumericType())
..suggestType(e.right, const AnyNumericType())
..suggestType(e.left, const AnyNumericType());
}
visitChildren(e);
}
@override
void visitUnaryExpression(UnaryExpression e) {
final operator = e.operator.type;
if (operator == TokenType.plus) {
// unary type does nothing, just returns the value
types.suggestSame(e, e.inner);
} else if (operator == TokenType.minus) {
types
..forceType(e, const AnyNumericType())
..suggestType(e.inner, const AnyNumericType());
}
visitChildren(e);
}
@override
void visitLiteral(Literal e) {
if (e is NullLiteral) {
types.forceType(e, const SqlType.nullType());
} else if (e is NumericLiteral) {
if (e.number.toInt() == e.number) {
types.forceType(e, const SqlType.int());
} else {
types.forceType(e, const SqlType.real());
}
if (e is BooleanLiteral) {
types.addTypeHint(e, const IsBoolean());
}
}
visitChildren(e);
super.visitChildren(e);
}
}

View File

@ -1,62 +1,29 @@
part of '../analysis.dart';
/// A type that sql expressions can have at runtime.
abstract class SqlType {
const SqlType();
const factory SqlType.nullType() = NullType._;
const factory SqlType.int() = IntegerType._;
const factory SqlType.real() = RealType._;
const factory SqlType.text() = TextType._;
const factory SqlType.blob() = BlobType._;
bool isSubTypeOf(SqlType other);
enum BasicType {
nullType,
int,
real,
text,
blob,
}
class NullType extends SqlType {
const NullType._();
class ResolvedType {
final BasicType type;
final TypeHint hint;
final bool nullable;
@override
bool isSubTypeOf(SqlType other) => true;
}
const ResolvedType({this.type, this.hint, this.nullable = false});
const ResolvedType.bool()
: this(type: BasicType.int, hint: const IsBoolean());
class IntegerType extends SqlType {
const IntegerType._();
@override
bool isSubTypeOf(SqlType other) => other is IntegerType;
}
class RealType extends SqlType {
const RealType._();
@override
bool isSubTypeOf(SqlType other) => other is RealType;
}
class TextType extends SqlType {
const TextType._();
@override
bool isSubTypeOf(SqlType other) => other is TextType;
}
class BlobType extends SqlType {
const BlobType._();
@override
bool isSubTypeOf(SqlType other) => other is BlobType;
}
class AnyNumericType extends SqlType {
const AnyNumericType();
@override
bool isSubTypeOf(SqlType other) {
return other is RealType || other is IntegerType;
ResolvedType withNullable(bool nullable) {
return ResolvedType(type: type, hint: hint, nullable: nullable);
}
}
/// Provides more precise hints than the [SqlType]. For instance, booleans are
/// Provides more precise hints than the [BasicType]. For instance, booleans are
/// stored as ints in sqlite, but it might be desirable to know whether an
/// expression will actually be a boolean.
abstract class TypeHint {

View File

@ -1,49 +1,134 @@
part of '../analysis.dart';
/// Attempts to resolve types of expressions, result columns and variables.
/// As sqlite is pretty lenient at typing and pretty much accepts every value
/// everywhere, this
const comparisonOperators = [
TokenType.equal,
TokenType.doubleEqual,
TokenType.exclamationEqual,
TokenType.lessMore,
TokenType.and,
TokenType.or,
TokenType.less,
TokenType.lessEqual,
TokenType.more,
TokenType.moreEqual
];
class TypeResolver {
final Map<Typeable, _ResolvingState> _matchedStates = {};
final Map<Typeable, ResolveResult> _results = {};
_ResolvingState _stateFor(Typeable typeable) {
return _matchedStates.putIfAbsent(
typeable, () => _ResolvingState(typeable, this));
ResolveResult _cache<T extends Typeable>(
ResolveResult Function(T param) resolver, T typeable) {
if (_results.containsKey(typeable)) {
return _results[typeable];
}
final calculated = resolver(typeable);
if (calculated.type != null) {
_results[typeable] = calculated;
}
return calculated;
}
void finish() {}
ResolveResult resolveColumn(Column column) {
return _cache((column) {
if (column is TableColumn) {
// todo probably needs to be nullable when coming from a join?
return ResolveResult(column.type);
} else if (column is ExpressionColumn) {
return resolveExpression(column.expression);
}
/// Suggest that [t] should have the type [type].
void suggestType(Typeable t, SqlType type) {
_stateFor(t).suggested.add(type);
throw StateError('Unknown column $column');
}, column);
}
void suggestSame(Typeable a, Typeable b) {}
ResolveResult resolveExpression(Expression expr) {
return _cache((expr) {
if (expr is Literal) {
return resolveLiteral(expr);
} else if (expr is UnaryExpression) {
return resolveExpression(expr.inner);
} else if (expr is Parentheses) {
return resolveExpression(expr.expression);
} else if (expr is Variable) {
return const ResolveResult.needsContext();
} else if (expr is Reference) {
return resolveColumn(expr.resolved as Column);
} else if (expr is FunctionExpression) {
return resolveFunctionCall(expr);
} else if (expr is IsExpression) {
return const ResolveResult(ResolvedType.bool());
} else if (expr is BinaryExpression) {
final operator = expr.operator.type;
if (comparisonOperators.contains(operator)) {
return const ResolveResult(ResolvedType.bool());
} else {
final type = _encapsulate(expr.childNodes.cast(),
[BasicType.int, BasicType.real, BasicType.text, BasicType.blob]);
return ResolveResult(type);
}
} else if (expr is SubQuery) {
// todo
}
void suggestBool(Typeable t) {
final state = _stateFor(t);
state.suggested.add(const SqlType.int());
state.hints.add(const IsBoolean());
throw StateError('Unknown expression $expr');
}, expr);
}
/// Marks that [t] will definitely have the type [type].
void forceType(Typeable t, SqlType type) {
_stateFor(t).forced = type;
ResolveResult resolveLiteral(Literal l) {
return _cache((l) {
if (l is StringLiteral) {
return ResolveResult(
ResolvedType(type: l.isBinary ? BasicType.blob : BasicType.text));
} else if (l is NumericLiteral) {
if (l is BooleanLiteral) {
return const ResolveResult(ResolvedType.bool());
} else {
return ResolveResult(
ResolvedType(type: l.isInt ? BasicType.int : BasicType.real));
}
} else if (l is NullLiteral) {
return const ResolveResult(
ResolvedType(type: BasicType.nullType, nullable: true));
}
throw StateError('Unknown literal $l');
}, l);
}
/// Add an additional hint
void addTypeHint(Typeable t, TypeHint hint) {
_stateFor(t).hints.add(hint);
ResolveResult resolveFunctionCall(FunctionExpression call) {
// todo
return const ResolveResult.unknown();
}
/// Returns the type of an expression in [expressions] that has the highest
/// order in [types].
ResolvedType _encapsulate(
Iterable<Expression> expressions, List<BasicType> types) {
final argTypes = expressions
.map((expr) => resolveExpression(expr).type)
.where((t) => t != null);
final type = types.lastWhere((t) => argTypes.any((arg) => arg.type == t));
final notNull = argTypes.any((t) => !t.nullable);
return ResolvedType(type: type, nullable: !notNull);
}
}
class _ResolvingState {
final Typeable typeable;
final TypeResolver resolver;
class ResolveResult {
final ResolvedType type;
final bool needsContext;
final bool unknown;
final List<TypeHint> hints = [];
final List<SqlType> suggested = [];
SqlType forced;
_ResolvingState(this.typeable, this.resolver);
const ResolveResult(this.type)
: needsContext = false,
unknown = false;
const ResolveResult.needsContext()
: type = null,
needsContext = true,
unknown = false;
const ResolveResult.unknown()
: type = null,
needsContext = false,
unknown = true;
}

View File

@ -23,6 +23,8 @@ class NullLiteral extends Literal {
class NumericLiteral extends Literal {
final num number;
bool get isInt => number.toInt() == number;
NumericLiteral(this.number, Token token) : super(token);
@override

View File

@ -12,8 +12,9 @@ void main() {
});
test('correctly resolves return columns', () {
final id = TableColumn('id');
final content = TableColumn('content');
final id = const TableColumn('id', ResolvedType(type: BasicType.int));
final content =
const TableColumn('content', ResolvedType(type: BasicType.text));
final demoTable = Table(
name: 'demo',
@ -30,6 +31,14 @@ void main() {
expect(resolvedColumns.map((c) => c.name),
['id', 'content', 'id', 'content', '3 + 4']);
expect(resolvedColumns.map((c) => context.typeOf(c).type.type), [
BasicType.int,
BasicType.text,
BasicType.int,
BasicType.text,
BasicType.int,
]);
final firstColumn = select.columns[0] as ExpressionResultColumn;
final secondColumn = select.columns[1] as ExpressionResultColumn;
final from = select.from[0] as TableReference;