Skip to content

Commit

Permalink
Interpret numeric literals the way PostgreSQL does (almost)
Browse files Browse the repository at this point in the history
Numeric literals are now interpreted almost like they do in
PostgreSQL:

  - numerics without a decimal point that fit into 64 bits are
    interpreted as `std::int64`,
  - all other numeric literals are interpreted as `std::decimal`
  • Loading branch information
elprans committed Oct 24, 2018
1 parent 094fc18 commit e793fd3
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 58 deletions.
3 changes: 2 additions & 1 deletion edb/lang/edgeql/ast.py
Expand Up @@ -17,6 +17,7 @@
#


import decimal
import typing

from edb.lang.common import enum as s_enum
Expand Down Expand Up @@ -231,7 +232,7 @@ class FunctionCall(Expr):


class Constant(Expr):
value: typing.Union[int, str, float, bool, bytes]
value: typing.Union[int, str, float, bool, bytes, decimal.Decimal]


class Parameter(Expr):
Expand Down
22 changes: 12 additions & 10 deletions edb/lang/edgeql/compiler/expr.py
Expand Up @@ -20,6 +20,7 @@
"""EdgeQL non-statement expression compilation functions."""


import decimal
import typing

from edb.lang.common import ast
Expand All @@ -30,6 +31,7 @@

from edb.lang.schema import objtypes as s_objtypes
from edb.lang.schema import pointers as s_pointers
from edb.lang.schema import scalars as s_scalars
from edb.lang.schema import types as s_types
from edb.lang.schema import utils as s_utils

Expand Down Expand Up @@ -164,18 +166,20 @@ def compile_Constant(
else:
if isinstance(expr.value, str):
std_type = 'std::str'
elif isinstance(expr.value, decimal.Decimal):
std_type = 'std::decimal'
elif isinstance(expr.value, float):
std_type = 'std::float64'
elif isinstance(expr.value, bool):
std_type = 'std::bool'
elif isinstance(expr.value, int):
# Integer value out of int64 bounds, use decimal
if expr.value > 2 ** 63 - 1 or expr.value < -2 ** 63:
std_type = 'std::decimal'
else:
std_type = 'std::int64'
elif isinstance(expr.value, bytes):
std_type = 'std::bytes'
elif isinstance(expr.value, int):
# If integer value is out of int64 bounds, use decimal
if -2 ** 63 <= expr.value < 2 ** 63:
std_type = 'std::int64'
else:
std_type = 'std::decimal'
else:
raise NotImplementedError(
f'unexpected value type in Constant AST: {type(expr.value)}')
Expand Down Expand Up @@ -567,7 +571,6 @@ def try_fold_arithmetic_binop(
schema = ctx.schema

real_t = schema.get('std::anyreal')
float_t = schema.get('std::anyfloat')
int_t = schema.get('std::anyint')

left_type = irutils.infer_type(left, schema)
Expand All @@ -576,9 +579,8 @@ def try_fold_arithmetic_binop(
if not left_type.issubclass(real_t) or not right_type.issubclass(real_t):
return

result_type = left_type
if right_type.issubclass(float_t):
result_type = right_type
result_type = s_scalars.get_op_type(
op, left_type, right_type, schema=schema)

left = left.expr
right = right.expr
Expand Down
3 changes: 2 additions & 1 deletion edb/lang/edgeql/compiler/setgen.py
Expand Up @@ -609,7 +609,8 @@ def ensure_set(
typehint is not None):
irutils.amend_empty_set_type(expr, typehint, schema=ctx.schema)

if typehint is not None and not expr.scls.issubclass(typehint):
if (typehint is not None and
not expr.scls.implicitly_castable_to(typehint, ctx.schema)):
raise errors.EdgeQLError(
f'expecting expression of type {typehint.name}, '
f'got {expr.scls.name}',
Expand Down
11 changes: 9 additions & 2 deletions edb/lang/edgeql/parser/grammar/expressions.py
Expand Up @@ -19,6 +19,8 @@

import ast as pyast
import collections
import decimal
import numbers
import re

from edb.lang.common import ast
Expand Down Expand Up @@ -621,7 +623,12 @@ def reduce_PLUS_Expr(self, *kids):

@parsing.precedence(precedence.P_UMINUS)
def reduce_MINUS_Expr(self, *kids):
self.val = qlast.UnaryOp(op=ast.ops.UMINUS, operand=kids[1].val)
operand = kids[1].val
if (isinstance(operand, qlast.Constant) and
isinstance(operand.value, numbers.Number)):
self.val = qlast.Constant(value=-operand.value)
else:
self.val = qlast.UnaryOp(op=ast.ops.UMINUS, operand=kids[1].val)

def reduce_Expr_PLUS_Expr(self, *kids):
self.val = qlast.BinOp(left=kids[0].val, op=ast.ops.ADD,
Expand Down Expand Up @@ -848,7 +855,7 @@ def reduce_ICONST(self, *kids):
self.val = qlast.Constant(value=int(kids[0].val))

def reduce_FCONST(self, *kids):
self.val = qlast.Constant(value=float(kids[0].val))
self.val = qlast.Constant(value=decimal.Decimal(kids[0].val))


class BaseStringConstant(Nonterm):
Expand Down
2 changes: 1 addition & 1 deletion edb/lang/schema/functions.py
Expand Up @@ -326,7 +326,7 @@ def _add_to_schema(self, schema):

if check_default_type:
default_type = irutils.infer_type(default, schema)
if not default_type.issubclass(p.type):
if not default_type.assignment_castable_to(p.type, schema):
raise ql_errors.EdgeQLError(
f'invalid declaration of parameter ${p.name} of '
f'function "{name}()": unexpected type of the default '
Expand Down
62 changes: 36 additions & 26 deletions edb/lang/schema/scalars.py
Expand Up @@ -126,49 +126,48 @@ def find_common_implicitly_castable_type(
'std::int64': 'std::float64',
'std::float32': 'std::float64',
'std::float64': 'std::decimal',
'std::decimal': None
'std::decimal': 'std::float64',
}


def _is_reachable(graph, source: str, target: str) -> bool:
if source == target:
return True

seen = set()
while True:
source = graph.get(source)
if source is None:
if source is None or source in seen:
return False
elif source == target:
return True
seen.add(source)


@functools.lru_cache()
def _is_implicitly_castable_impl(left: str, right: str) -> bool:
return _is_reachable(_implicit_numeric_cast_map, left, right)
def _is_implicitly_castable_impl(source: str, target: str) -> bool:
return _is_reachable(_implicit_numeric_cast_map, source, target)


@functools.lru_cache()
def _find_common_castable_type_impl(
left: str, right: str) -> typing.Optional[str]:
source: str, target: str) -> typing.Optional[str]:

if left == right:
return left
if left not in _implicit_numeric_cast_map:
return
if right not in _implicit_numeric_cast_map:
return
if _is_implicitly_castable_impl(target, source):
return source
if _is_implicitly_castable_impl(source, target):
return target

orig_left = left
# Elevate target in the castability ladder, and check if
# source is castable to it on each step.
seen = set()
while True:
left = _implicit_numeric_cast_map.get(left)
if left is None:
left = orig_left
new_right = _implicit_numeric_cast_map.get(right)
if new_right is None:
return right
right = new_right
if left == right:
return left
target = _implicit_numeric_cast_map.get(target)
if target is None or target in seen:
return None
elif _is_implicitly_castable_impl(source, target):
return target
seen.add(target)


# target -> source (source can be casted into target in assignment)
Expand Down Expand Up @@ -229,7 +228,7 @@ def _is_assignment_castable_impl(source: str, target: str) -> bool:
],

ast.ops.DIV: [
('std::int64', 'std::int64', 'std::float64'),
('std::int64', 'std::int64', 'std::int64'),
('std::float32', 'std::float32', 'std::float32'),
('std::float64', 'std::float64', 'std::float64'),
('std::decimal', 'std::decimal', 'std::decimal'),
Expand Down Expand Up @@ -295,21 +294,32 @@ def _get_op_type(op: ast.ops.Operator,
return None

operand_count = len(operands)
shortlist = []

for candidate in candidates:
if len(candidate) != operand_count + 1:
# Skip candidates with non-matching operand count.
continue

cast_count = 0

for def_opr_name, passed_opr in zip(candidate, operands):
def_opr = schema.get(def_opr_name)

if not (passed_opr.issubclass(def_opr) or
passed_opr.implicitly_castable_to(def_opr, schema)):
if passed_opr.issubclass(def_opr):
pass
elif passed_opr.implicitly_castable_to(def_opr, schema):
cast_count += 1
else:
break
else:
return schema.get(candidate[-1])
shortlist.append((candidate[-1], cast_count))

return None
if shortlist:
shortlist.sort(key=lambda c: c[1])
return schema.get(shortlist[0][0])
else:
return None


@functools.lru_cache()
Expand Down
20 changes: 20 additions & 0 deletions edb/lang/schema/types.py
Expand Up @@ -245,6 +245,13 @@ def implicitly_castable_to(self, other: Type, schema) -> bool:
return self.element_type.implicitly_castable_to(
other.element_type, schema)

def assignment_castable_to(self, other: Type, schema) -> bool:
if not isinstance(other, Array):
return False

return self.element_type.assignment_castable_to(
other.element_type, schema)

def find_common_implicitly_castable_type(
self, other: Type, schema) -> typing.Optional[Type]:

Expand Down Expand Up @@ -349,6 +356,19 @@ def implicitly_castable_to(self, other: Type, schema) -> bool:

return True

def assignment_castable_to(self, other: Type, schema) -> bool:
if not isinstance(other, Tuple):
return False

if len(self.element_types) != len(other.element_types):
return False

for st, ot in zip(self.element_types, other.element_types):
if not st.assignment_castable_to(ot, schema):
return False

return True

def find_common_implicitly_castable_type(
self, other: Type, schema) -> typing.Optional[Type]:

Expand Down
3 changes: 3 additions & 0 deletions edb/server/pgsql/codegen.py
Expand Up @@ -622,7 +622,10 @@ def visit_SortBy(self, node):
'unexpected NULLS order: {}'.format(node.nulls))

def visit_TypeCast(self, node):
# '::' has very high precedence, so parenthesize the expression.
self.write('(')
self.visit(node.arg)
self.write(')')
self.write('::')
self.visit(node.type_name)

Expand Down
6 changes: 3 additions & 3 deletions tests/test_edgeql_calls.py
Expand Up @@ -384,16 +384,16 @@ async def test_edgeql_calls_10(self):
{'std::float64'},

{'std::float64'},
{'std::decimal'},
{'std::float64'},
{'std::decimal'},

{'std::decimal'},
{'std::float64'},
{'std::float64'},
{'std::decimal'},

{'std::float64'},
{'std::float32'},
{'std::float64'},
{'std::decimal'},
])

async def test_edgeql_calls_11(self):
Expand Down

0 comments on commit e793fd3

Please sign in to comment.