diff --git a/.gitignore b/.gitignore index bd100911..b66c2e03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +*.DS_Store +*.pyc +.ipynb_checkpoints .idea __pycache__/ docs/.DS_Store diff --git a/README.md b/README.md index d4d3913f..257685ff 100644 --- a/README.md +++ b/README.md @@ -24,21 +24,26 @@ a language created at Google earlier. ## Why? Logica is for engineers, data scientists and other specialists who want to use -logic programming syntax when writing queries and pipelines to run on -[BigQuery](https://cloud.google.com/bigquery). +logic programming syntax when writing queries and pipelines for databases and datawarehouses. +Logica programs run on +[BigQuery](https://cloud.google.com/bigquery), [Postgres](https://postgresql.org) and [SQLite](https://www.sqlite.org/). -Logica compiles to StandardSQL and gives you access to the power of BigQuery -engine with the convenience of logic programming syntax. This is useful because -BigQuery is magnitudes more powerful than state of the art native -logic programming engines. +Logica compiles to SQL and gives you access to the power of SQL ecosystem +with the convenience of logic programming syntax. + +This is useful because +SQL enginers are magnitudes more powerful than state of the art native +logic programming engines. For example, BigQuery is a distributed datawarehouse and thus logic programs written +in Logica can be easily parallelized onto thousands of servers. Postgres and SQLite are among most popular databases, they are +capable of processing substantial volumes of data right on your machine. We encourage you to try Logica, especially if * you already use logic programming and need more computational power, **or** -* you use SQL, but feel unsatisfied about its readability, **or** +* you already have data in BigQuery, PostgreSQL or SQLite, **or** * you want to learn logic programming and apply it to processing of Big Data. -In the future we plan to support more SQL dialects and engines. +Support for more SQL dialects and engines is coming in the future. ## I have not heard of logic programming. What is it? diff --git a/colab_logica.py b/colab_logica.py index cfe4b34e..8efe885a 100755 --- a/colab_logica.py +++ b/colab_logica.py @@ -16,13 +16,21 @@ """Library for using Logica in CoLab.""" +from decimal import Decimal +import getpass +import json +import re + from .common import color from .common import concertina_lib +from .common import psql_logica from .compiler import functors from .compiler import rule_translate from .compiler import universe +from .type_inference.research import infer + import IPython from IPython.core.magic import register_cell_magic @@ -70,6 +78,13 @@ PREAMBLE = None +if hasattr(concertina_lib, 'graphviz'): + DISPLAY_MODE = 'colab' +else: + DISPLAY_MODE = 'colab-text' + +DEFAULT_ENGINE = 'bigquery' + def SetPreamble(preamble): global PREAMBLE @@ -83,6 +98,12 @@ def SetDbConnection(connection): global DB_CONNECTION DB_CONNECTION = connection +def ConnectToPostgres(mode='interactive'): + connection = psql_logica.ConnectToPostgres(mode) + SetDbConnection(connection) + global DEFAULT_ENGINE + DEFAULT_ENGINE = 'psql' + def EnsureAuthenticatedUser(): global USER_AUTHENTICATED global PROJECT @@ -143,9 +164,14 @@ def RunSQL(sql, engine, connection=None, is_final=False): return client.query(sql).to_dataframe() elif engine == 'psql': if is_final: - return pandas.read_sql(sql, connection) + cursor = psql_logica.PostgresExecute(sql, connection) + rows = cursor.fetchall() + df = pandas.DataFrame( + rows, columns=[d[0] for d in cursor.description]) + df = df.applymap(psql_logica.DigestPsqlType) + return df else: - return connection.execute(sql) + psql_logica.PostgresExecute(sql, connection) elif engine == 'sqlite': try: if is_final: @@ -164,6 +190,15 @@ def RunSQL(sql, engine, connection=None, is_final=False): 'for now.') +def Ingress(table_name, csv_file_name): + with open(csv_file_name) as csv_data_io: + cursor = DB_CONNECTION.cursor() + cursor.copy_expert( + 'COPY %s FROM STDIN WITH CSV HEADER' % table_name, + csv_data_io) + DB_CONNECTION.commit() + + class SqliteRunner(object): def __init__(self): self.connection = sqlite3_logica.SqliteConnect() @@ -177,13 +212,17 @@ class PostgresRunner(object): def __init__(self): global DB_CONNECTION global DB_ENGINE - if DB_CONNECTION: - self.engine = DB_ENGINE - self.connection = DB_CONNECTION - else: - (self.engine, self.connection) = PostgresJumpStart() - DB_ENGINE = self.engine - DB_CONNECTION = self.connection + if not DB_CONNECTION: + print("Assuming this is running on Google CoLab in a temporary") + print("environment.") + print("Would you like to install and run postgres?") + user_choice = input('y or N? ') + if user_choice != 'y': + print('User declined.') + print('Bailing out.') + return + PostgresJumpStart() + self.connection = DB_CONNECTION def __call__(self, sql, engine, is_final): return RunSQL(sql, engine, self.connection, is_final) @@ -206,13 +245,18 @@ def Logica(line, cell, run_query): e.ShowMessage() return try: - program = universe.LogicaProgram(parsed_rules) + program = universe.LogicaProgram( + parsed_rules, + user_flags={'logica_default_engine': DEFAULT_ENGINE}) except functors.FunctorError as e: e.ShowMessage() return except rule_translate.RuleCompileException as e: e.ShowMessage() return + except infer.TypeErrorCaughtException as e: + e.ShowMessage() + return engine = program.annotations.Engine() @@ -269,9 +313,13 @@ def Logica(line, cell, run_query): else: raise Exception('Logica only supports BigQuery, PostgreSQL and SQLite ' 'for now.') - - result_map = concertina_lib.ExecuteLogicaProgram( - executions, sql_runner=sql_runner, sql_engine=engine) + try: + result_map = concertina_lib.ExecuteLogicaProgram( + executions, sql_runner=sql_runner, sql_engine=engine, + display_mode=DISPLAY_MODE) + except infer.TypeErrorCaughtException as e: + e.ShowMessage() + return for idx, predicate in enumerate(predicates): t = result_map[predicate] @@ -320,18 +368,21 @@ def PostgresJumpStart(): # Connect to the database. from logica import colab_logica -from sqlalchemy import create_engine -import pandas -engine = create_engine('postgresql+psycopg2://logica:logica@127.0.0.1', pool_recycle=3600); -connection = engine.connect(); -colab_logica.SetDbConnection(connection)""") +import psycopg2 +connection = psycopg2.connect(host='localhost', database='logica', user='logica', password='logica') +connection.autocommit = True +colab_logica.DEFAULT_ENGINE = 'psql' +colab_logica.SetDbConnection(connection) +""") return print('Installation succeeded. Connecting...') # Connect to the database. - from logica import colab_logica - from sqlalchemy import create_engine - import pandas - engine = create_engine('postgresql+psycopg2://logica:logica@127.0.0.1', pool_recycle=3600) - connection = engine.connect() + import psycopg2 + connection = psycopg2.connect(host='localhost', database='logica', user='logica', password='logica') + connection.autocommit = True + print('Connected.') - return engine, connection + global DEFAULT_ENGINE + global DB_CONNECTION + DEFAULT_ENGINE = 'psql' + DB_CONNECTION = connection diff --git a/common/concertina_lib.py b/common/concertina_lib.py index 94a96741..e4ff2b4c 100644 --- a/common/concertina_lib.py +++ b/common/concertina_lib.py @@ -4,10 +4,18 @@ try: import graphviz +except: + pass + # This is annoying to see in terminal each time. + # Consider adding back if lack of messaging is confusing. + # print('Could not import graphviz tools in Concertina.') + +try: + from IPython.display import HTML from IPython.display import display from IPython.display import update_display except: - print('Could not import CoLab tools in Concertina.') + print('Could not import IPython in Concertina.') if '.' not in __package__: from common import graph_art @@ -33,7 +41,7 @@ def Run(self, action): is_final=(predicate in self.final_predicates)) end = datetime.datetime.now() if self.print_running_predicate: - print(' (%d seconds)' % (end - start).seconds) + print(' (%d ms)' % int((end - start).total_seconds() * 1000)) if predicate in self.final_predicates: self.final_result[predicate] = result @@ -75,7 +83,7 @@ def __init__(self, config, engine, display_mode='colab'): self.all_actions = {a["name"] for a in self.config} self.complete_actions = set() self.running_actions = set() - assert display_mode in ('colab', 'terminal'), ( + assert display_mode in ('colab', 'terminal', 'colab-text'), ( 'Unrecognized display mode: %s' % display_mode) self.display_mode = display_mode self.display_id = self.GetDisplayId() @@ -137,7 +145,14 @@ def AsNodesAndEdges(self): """Nodes and edges to display in terminal.""" def ColoredNode(node): if node in self.running_actions: - return '\033[1m\033[93m' + node + '\033[0m' + if self.display_mode == 'terminal': + return '\033[1m\033[93m' + node + '\033[0m' + elif self.display_mode == 'colab-text': + return ( + '' + node + '' + ) + else: + assert False, self.display_mode else: return node nodes = [] @@ -150,11 +165,24 @@ def ColoredNode(node): edges.append([prerequisite_node, a_node]) return nodes, edges + def StateAsSimpleHTML(self): + style = ';'.join([ + 'border: 1px solid rgba(0, 0, 0, 0.3)', + 'width: fit-content;', + 'padding: 20px', + 'border-radius: 5px', + 'box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2)']) + return HTML('
%s
' % ( + style, self.AsTextPicture(updating=False))) + def Display(self): if self.display_mode == 'colab': display(self.AsGraphViz(), display_id=self.display_id) elif self.display_mode == 'terminal': print(self.AsTextPicture(updating=False)) + elif self.display_mode == 'colab-text': + display(self.StateAsSimpleHTML(), + display_id=self.display_id) else: assert 'Unexpected mode:', self.display_mode @@ -163,6 +191,10 @@ def UpdateDisplay(self): update_display(self.AsGraphViz(), display_id=self.display_id) elif self.display_mode == 'terminal': print(self.AsTextPicture(updating=True)) + elif self.display_mode == 'colab-text': + update_display( + self.StateAsSimpleHTML(), + display_id=self.display_id) else: assert 'Unexpected mode:', self.display_mode @@ -261,10 +293,14 @@ def ConcertinaConfig(table_to_export_map, dependency_edges, print_running_predicate=(display_mode != 'terminal')) preambles = set(e.preamble for e in logica_executions) - assert len(preambles) == 1, 'Inconsistent preambles: %s' % preambles - [preamble] = list(preambles) - if preamble: - sql_runner(preamble, sql_engine, is_final=False) + # Due to change of types from predicate to predicate preables are not + # consistent. However we expect preambles to be idempotent. + # So we simply run all of them. + # assert len(preambles) == 1, 'Inconsistent preambles: %s' % preambles + # [preamble] = list(preambles) + for preamble in preambles: + if preamble: + sql_runner(preamble, sql_engine, is_final=False) concertina = Concertina(config, engine, display_mode=display_mode) concertina.Run() diff --git a/common/logica_test.py b/common/logica_test.py index 987027ec..ee04ac2f 100644 --- a/common/logica_test.py +++ b/common/logica_test.py @@ -18,13 +18,18 @@ """Utilities for YotaQL tests.""" import subprocess +import json if '.' not in __package__: from common import color from common import logica_lib + from type_inference.research import infer + from parser_py import parse else: from ..common import color from ..common import logica_lib + from ..type_inference.research import infer + from ..parser_py import parse class TestManager(object): @@ -54,6 +59,47 @@ def RunTest(cls, name, src, predicate, golden, user_flags, cls.GOLDEN_RUN, cls.ANNOUNCE_TESTS, import_root) + @classmethod + def RunTypesTest(cls, name, src=None, golden=None): + if cls.RUN_ONLY and name not in cls.RUN_ONLY: + return + RunTypesTest(name, src, golden, + overwrite=cls.GOLDEN_RUN) + + +def RunTypesTest(name, src=None, golden=None, + overwrite=False): + src = src or (name + '.l') + golden = golden or (name + '.txt') + + test_result = '{warning}RUNNING{end}' + print(color.Format('% 50s %s' % (name, test_result))) + + program_text = open(src).read() + try: + parsed_rules = parse.ParseFile(program_text)['rule'] + except parse.ParsingException as parsing_exception: + parsing_exception.ShowMessage() + sys.exit(1) + + typing_engine = infer.TypesInferenceEngine(parsed_rules) + typing_engine.InferTypes() + result = json.dumps(parsed_rules, sort_keys=True, indent=' ') + + if overwrite: + with open(golden, 'w') as w: + w.write(result) + golden_result = open(golden).read() + + if result == golden_result: + test_result = '{ok}PASSED{end}' + else: + p = subprocess.Popen(['diff', '-', golden], stdin=subprocess.PIPE) + p.communicate(result.encode()) + test_result = '{error}FAILED{end}' + + print('\033[F\033[K' + color.Format('% 50s %s' % (name, test_result))) + def RunTest(name, src, predicate, golden, user_flags=None, diff --git a/common/psql_logica.py b/common/psql_logica.py new file mode 100644 index 00000000..443aeda6 --- /dev/null +++ b/common/psql_logica.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + +import getpass +import json +import os +import re +from decimal import Decimal + +if '.' not in __package__: + from type_inference.research import infer +else: + from ..type_inference.research import infer + + +def PostgresExecute(sql, connection): + import psycopg2 + import psycopg2.extras + cursor = connection.cursor() + try: + cursor.execute(sql) + # Make connection aware of the used types. + types = re.findall(r'-- Logica type: (\w*)', sql) + for t in types: + if t != 'logicarecord893574736': # Empty record. + psycopg2.extras.register_composite(t, cursor, globally=True) + except psycopg2.errors.UndefinedTable as e: + raise infer.TypeErrorCaughtException( + infer.ContextualizedError.BuildNiceMessage( + 'Running SQL.', 'Undefined table used: ' + str(e))) + except psycopg2.Error as e: + connection.rollback() + raise e + return cursor + + +def DigestPsqlType(x): + if isinstance(x, tuple): + return PsqlTypeAsDictionary(x) + if isinstance(x, list) and len(x) > 0: + return PsqlTypeAsList(x) + if isinstance(x, Decimal): + if x.as_integer_ratio()[1] == 1: + return int(x) + else: + return float(x) + return x + + +def PsqlTypeAsDictionary(record): + result = {} + for f in record._asdict(): + a = getattr(record, f) + result[f] = DigestPsqlType(a) + return result + + +def PsqlTypeAsList(a): + return list(map(DigestPsqlType, a)) + + +def ConnectToPostgres(mode): + import psycopg2 + if mode == 'interactive': + print('Please enter PostgreSQL URL, or config in JSON format with fields host, database, user and password.') + connection_str = getpass.getpass() + elif mode == 'environment': + connection_str = os.environ.get('LOGICA_PSQL_CONNECTION') + assert connection_str, ( + 'Please provide PSQL connection parameters ' + 'in LOGICA_PSQL_CONNECTION.') + else: + assert False, 'Unknown mode:' + mode + if connection_str.startswith('postgres'): + connection = psycopg2.connect(connection_str) + else: + connection_json = json.loads(connection_str) + connection = psycopg2.connect(**connection_json) + + connection.autocommit = True + return connection \ No newline at end of file diff --git a/compiler/dialect_libraries/bq_library.py b/compiler/dialect_libraries/bq_library.py index d6cc3546..6c794716 100644 --- a/compiler/dialect_libraries/bq_library.py +++ b/compiler/dialect_libraries/bq_library.py @@ -17,6 +17,7 @@ library = """ ->(left:, right:) = {arg: left, value: right}; `=`(left:, right:) = right :- left == right; +`~`(left:, right:); # No action. Compiler unifies types. # All ORDER BY arguments are wrapped, to avoid confusion with # column index. diff --git a/compiler/dialect_libraries/presto_library.py b/compiler/dialect_libraries/presto_library.py index 90da11d4..c7015bc4 100644 --- a/compiler/dialect_libraries/presto_library.py +++ b/compiler/dialect_libraries/presto_library.py @@ -17,6 +17,7 @@ library = """ ->(left:, right:) = {arg: left, value: right}; `=`(left:, right:) = right :- left == right; +`~`(left:, right:); # No action. Compiler unifies types. ArgMin(a) = SqlExpr("(ARRAY_AGG({arg} order by {value}))[1]", {arg: a.arg, value: a.value}); diff --git a/compiler/dialect_libraries/psql_library.py b/compiler/dialect_libraries/psql_library.py index 2bd1e31b..db52216b 100644 --- a/compiler/dialect_libraries/psql_library.py +++ b/compiler/dialect_libraries/psql_library.py @@ -17,13 +17,14 @@ library = """ ->(left:, right:) = {arg: left, value: right}; `=`(left:, right:) = right :- left == right; +`~`(left:, right:); # No action. Compiler unifies types. -ArgMin(a) = SqlExpr("(ARRAY_AGG({arg} order by {value}))[1]", - {arg: a.arg, value: a.value}); +ArgMin(a) = (SqlExpr("(ARRAY_AGG({arg} order by {value}))[1]", + {arg: {argpod: a.arg}, value: a.value})).argpod; -ArgMax(a) = SqlExpr( +ArgMax(a) = (SqlExpr( "(ARRAY_AGG({arg} order by {value} desc))[1]", - {arg: a.arg, value: a.value}); + {arg: {argpod: a.arg}, value: a.value})).argpod; ArgMaxK(a, l) = SqlExpr( "(ARRAY_AGG({arg} order by {value} desc))[1:{lim}]", @@ -37,4 +38,11 @@ "ARRAY_AGG({value} order by {arg})", {arg: a.arg, value: a.value}); +RecordAsJson(r) = SqlExpr( + "ROW_TO_JSON({r})", {r:}); + +Fingerprint(s) = SqlExpr("('x' || substr(md5({s}), 1, 16))::bit(64)::bigint", {s:}); + +Num(a) = a; +Str(a) = a; """ diff --git a/compiler/dialect_libraries/sqlite_library.py b/compiler/dialect_libraries/sqlite_library.py index 284a94d7..1a4f4eb0 100644 --- a/compiler/dialect_libraries/sqlite_library.py +++ b/compiler/dialect_libraries/sqlite_library.py @@ -17,6 +17,7 @@ library = """ ->(left:, right:) = {arg: left, value: right}; `=`(left:, right:) = right :- left == right; +`~`(left:, right:); # No action. Compiler unifies types. Arrow(left, right) = arrow :- left == arrow.arg, diff --git a/compiler/dialect_libraries/trino_library.py b/compiler/dialect_libraries/trino_library.py index 90da11d4..c7015bc4 100644 --- a/compiler/dialect_libraries/trino_library.py +++ b/compiler/dialect_libraries/trino_library.py @@ -17,6 +17,7 @@ library = """ ->(left:, right:) = {arg: left, value: right}; `=`(left:, right:) = right :- left == right; +`~`(left:, right:); # No action. Compiler unifies types. ArgMin(a) = SqlExpr("(ARRAY_AGG({arg} order by {value}))[1]", {arg: a.arg, value: a.value}); diff --git a/compiler/dialects.py b/compiler/dialects.py index c1e0950e..a281f821 100755 --- a/compiler/dialects.py +++ b/compiler/dialects.py @@ -40,6 +40,13 @@ def Get(engine): class Dialect(object): pass + # Default methods: + def MaybeCascadingDeletionWord(self): + return '' # No CASCADE is needed by default. + + def PredicateLiteral(self, predicate_name): + return "'predicate_name:%s'" % predicate_name + class BigQueryDialect(Dialect): """BigQuery SQL dialect.""" @@ -55,7 +62,7 @@ def InfixOperators(self): '++': 'CONCAT(%s, %s)', } - def Subscript(self, record, subscript): + def Subscript(self, record, subscript, record_is_table): return '%s.%s' % (record, subscript) def LibraryProgram(self): @@ -73,6 +80,10 @@ def GroupBySpecBy(self): def DecorateCombineRule(self, rule, var): return rule + def PredicateLiteral(self, predicate_name): + return 'STRUCT("%s" AS predicate_name)' % predicate_name + + class SqLiteDialect(Dialect): """SqLite SQL dialect.""" @@ -102,74 +113,7 @@ def BuiltInFunctions(self): } def DecorateCombineRule(self, rule, var): - """Resolving ambiguity of aggregation scope.""" - # Entangling result of aggregation with a variable that comes from a list - # unnested inside a combine expression, to make it clear that aggregation - # must be done in the combine. - rule = copy.deepcopy(rule) - - rule['head']['record']['field_value'][0]['value'][ - 'aggregation']['expression']['call'][ - 'record']['field_value'][0]['value'] = ( - { - 'expression': { - 'call': { - 'predicate_name': 'MagicalEntangle', - 'record': { - 'field_value': [ - { - 'field': 0, - 'value': rule['head']['record']['field_value'][0]['value'][ - 'aggregation']['expression']['call'][ - 'record']['field_value'][0]['value'] - }, - { - 'field': 1, - 'value': { - 'expression': { - 'variable': { - 'var_name': var - } - } - } - } - ] - } - } - } - } - ) - - if 'body' not in rule: - rule['body'] = {'conjunction': {'conjunct': []}} - rule['body']['conjunction']['conjunct'].append( - { - "inclusion": { - "list": { - "literal": { - "the_list": { - "element": [ - { - "literal": { - "the_number": { - "number": "0" - } - } - } - ] - } - } - }, - "element": { - "variable": { - "var_name": var - } - } - } - } - ) - return rule - + return DecorateCombineRule(rule, var) def InfixOperators(self): return { @@ -178,9 +122,12 @@ def InfixOperators(self): 'in': 'IN_LIST(%s, %s)' } - def Subscript(self, record, subscript): - return 'JSON_EXTRACT(%s, "$.%s")' % (record, subscript) - + def Subscript(self, record, subscript, record_is_table): + if record_is_table: + return '%s.%s' % (record, subscript) + else: + return 'JSON_EXTRACT(%s, "$.%s")' % (record, subscript) + def LibraryProgram(self): return sqlite_library.library @@ -203,9 +150,13 @@ def BuiltInFunctions(self): return { 'Range': '(SELECT ARRAY_AGG(x) FROM GENERATE_SERIES(0, {0} - 1) as x)', 'ToString': 'CAST(%s AS TEXT)', + 'ToInt64': 'CAST(%s AS BIGINT)', 'Element': '({0})[{1} + 1]', - 'Size': 'ARRAY_LENGTH(%s, 1)', - 'Count': 'COUNT(DISTINCT {0})' + 'Size': 'COALESCE(ARRAY_LENGTH({0}, 1), 0)', + 'Count': 'COUNT(DISTINCT {0})', + 'MagicalEntangle': '(CASE WHEN {1} = 0 THEN {0} ELSE NULL END)', + 'ArrayConcat': '{0} || {1}', + 'Split': 'STRING_TO_ARRAY({0}, {1})' } def InfixOperators(self): @@ -213,7 +164,7 @@ def InfixOperators(self): '++': 'CONCAT(%s, %s)', } - def Subscript(self, record, subscript): + def Subscript(self, record, subscript, record_is_table): return '(%s).%s' % (record, subscript) def LibraryProgram(self): @@ -226,10 +177,13 @@ def ArrayPhrase(self): return 'ARRAY[%s]' def GroupBySpecBy(self): - return 'name' + return 'expr' def DecorateCombineRule(self, rule, var): - return rule + return DecorateCombineRule(rule, var) + + def MaybeCascadingDeletionWord(self): + return ' CASCADE' # Need to cascade in PSQL. class Trino(Dialect): @@ -253,7 +207,7 @@ def InfixOperators(self): '++': 'CONCAT(%s, %s)', } - def Subscript(self, record, subscript): + def Subscript(self, record, subscript, record_is_table): return '%s.%s' % (record, subscript) def LibraryProgram(self): @@ -291,7 +245,7 @@ def InfixOperators(self): '++': 'CONCAT(%s, %s)', } - def Subscript(self, record, subscript): + def Subscript(self, record, subscript, record_is_table): return '%s.%s' % (record, subscript) def LibraryProgram(self): @@ -309,6 +263,74 @@ def GroupBySpecBy(self): def DecorateCombineRule(self, rule, var): return rule +def DecorateCombineRule(rule, var): + """Resolving ambiguity of aggregation scope.""" + # Entangling result of aggregation with a variable that comes from a list + # unnested inside a combine expression, to make it clear that aggregation + # must be done in the combine. + rule = copy.deepcopy(rule) + + rule['head']['record']['field_value'][0]['value'][ + 'aggregation']['expression']['call'][ + 'record']['field_value'][0]['value'] = ( + { + 'expression': { + 'call': { + 'predicate_name': 'MagicalEntangle', + 'record': { + 'field_value': [ + { + 'field': 0, + 'value': rule['head']['record']['field_value'][0]['value'][ + 'aggregation']['expression']['call'][ + 'record']['field_value'][0]['value'] + }, + { + 'field': 1, + 'value': { + 'expression': { + 'variable': { + 'var_name': var + } + } + } + } + ] + } + } + } + } + ) + + if 'body' not in rule: + rule['body'] = {'conjunction': {'conjunct': []}} + rule['body']['conjunction']['conjunct'].append( + { + "inclusion": { + "list": { + "literal": { + "the_list": { + "element": [ + { + "literal": { + "the_number": { + "number": "0" + } + } + } + ] + } + } + }, + "element": { + "variable": { + "var_name": var + } + } + } + } + ) + return rule class Databricks(Dialect): """Databricks dialect""" diff --git a/compiler/expr_translate.py b/compiler/expr_translate.py index 4b7ef5c9..c9bb299a 100755 --- a/compiler/expr_translate.py +++ b/compiler/expr_translate.py @@ -39,6 +39,7 @@ class QL(object): 'ToUInt64': 'CAST(%s AS UINT64)', 'ToString': 'CAST(%s AS STRING)', # Aggregation. + '1': 'MIN(%s)', # Should be ANY_VALUE, but using MIN, so it works everywhere. 'Aggr': '%s', # Placeholder to use formulas for aggregation. 'Agg+': 'SUM(%s)', 'Agg++': 'ARRAY_CONCAT_AGG(%s)', @@ -96,8 +97,8 @@ class QL(object): '&&': '%s AND %s', '%': 'MOD(%s, %s)' } - BULK_FUNCTIONS = {} - BULK_FUNCTIONS_ARITY_RANGE = {} + BULK_FUNCTIONS = None + BULK_FUNCTIONS_ARITY_RANGE = None # When adding any analytic functions please check that ConvertAnalytic # function handles them correctly. @@ -188,18 +189,22 @@ def CamelCase(s): reader = processed_functions.GetCsv() header = next(reader) + bulk_functions = {} + bulk_functions_arity_range = {} for row in reader: row = dict(list(zip(header, row))) if row['function'][0] == '$': # TODO: Process operators. continue function_name = CamelCase(row['function']) - cls.BULK_FUNCTIONS[function_name] = ( + bulk_functions[function_name] = ( '%s(%s)' % (row['sql_function'], '%s')) - cls.BULK_FUNCTIONS_ARITY_RANGE[function_name] = ( + bulk_functions_arity_range[function_name] = ( int(row['min_args']), float('inf') if row['has_repeated_args'] == '1' else int(row['max_args'])) + cls.BULK_FUNCTIONS = bulk_functions + cls.BULK_FUNCTIONS_ARITY_RANGE = bulk_functions_arity_range def BuiltInFunctionArityRange(self, f): """Returns arity of the built-in function.""" @@ -235,10 +240,10 @@ def Function(self, f, args): def Infix(self, op, args): return op % (args['left'], args['right']) - def Subscript(self, record, subscript): + def Subscript(self, record, subscript, record_is_table): if isinstance(subscript, int): subscript = 'col%d' % subscript - return self.dialect.Subscript(record, subscript) + return self.dialect.Subscript(record, subscript, record_is_table) def IntLiteral(self, literal): return str(literal['number']) @@ -254,8 +259,13 @@ def ListLiteralInternals(self, literal): return ', '.join([self.ConvertToSql(e) for e in literal['element']]) - def ListLiteral(self, literal): - return self.dialect.ArrayPhrase() % self.ListLiteralInternals(literal) + def ListLiteral(self, literal, element_type_name): + suffix = ('::' + element_type_name + '[]' + if self.dialect.Name() == 'PostgreSQL' + else '') + return ( + self.dialect.ArrayPhrase() % + self.ListLiteralInternals(literal)) + suffix def BoolLiteral(self, literal): return literal['the_bool'] @@ -267,9 +277,35 @@ def NullLiteral(self, literal): def PredicateLiteral(self, literal): if self.convert_to_json: return '{"predicate_name": "%s"}' % (literal['predicate_name']) - return 'STRUCT("%s" AS predicate_name)' % literal['predicate_name'] - - def Variable(self, variable): + return self.dialect.PredicateLiteral(literal['predicate_name']) + + def VariableMaybeTableSQLite(self, variable, expression_type): + def MakeSubscript(subscripted_variable, subscripted_field): + return {'expression': + {'subscript': {'record': {'variable': + {'var_name': subscripted_variable, + 'dont_expand': True}}, + 'subscript': { + 'literal': {'the_symbol': { + 'symbol': subscripted_field}}}}}} + expr = self.vocabulary[variable['var_name']] + if '.' not in expr and 'dont_expand' not in variable: + + if not isinstance(expression_type, dict): + raise self.exception_maker( + 'Could not create record ' + color.Warn(expr) + + '. Type inference is ' + + 'required to convert table rows to records in SQLite.') + return self.ConvertToSql({'record': {'field_value': [ + {'field': k, + 'value': MakeSubscript(variable['var_name'], k)} + for k in sorted(expression_type) + ]}}) + return expr + + def Variable(self, variable, expression_type): + if self.dialect.Name() == 'SqLite': + return self.VariableMaybeTableSQLite(variable, expression_type) if variable['var_name'] in self.vocabulary: return self.vocabulary[variable['var_name']] else: @@ -294,7 +330,7 @@ def RecordAsJson(self, record): value=self.ConvertToSql(f_v['value']['expression']))) return '{%s}' % ', '.join(json_field_values) - def Record(self, record): + def Record(self, record, record_type=None): if self.convert_to_json: return self.RecordAsJson(record) # TODO: Move this to dialects.py. @@ -310,6 +346,12 @@ def Record(self, record): for f_v in record['field_value']) if self.dialect.Name() == 'Trino': return '(SELECT %s)' % arguments_str + if self.dialect.Name() == 'PostgreSQL': + assert record_type, json.dumps(record, indent=' ') + args = ', '.join( + self.ConvertToSql(f_v['value']['expression']) + for f_v in sorted(record['field_value'], key=lambda x: x['field'])) + return 'ROW(%s)::%s' % (args, record_type) return 'STRUCT(%s)' % arguments_str def GenericSqlExpression(self, record): @@ -418,12 +460,29 @@ def GetValueOfField(field_values, field): 'implication': {'if_then': new_if_thens, 'otherwise': new_otherwise}} return self.ConvertToSql(new_expr) + def ConvertToSqlForGroupBy(self, expression): + if 'literal' in expression and 'the_string' in expression['literal']: + # To calm down PSQL: + return f"({self.ConvertToSql(expression)} || '')" + return self.ConvertToSql(expression) + + def ExpressionIsTable(self, expression): + return ( + 'variable' in expression and + 'dont_expand' in expression['variable'] and + self.VariableIsTable(expression['variable']['var_name'])) + + def VariableIsTable(self, variable_name): + return '.' not in self.vocabulary[variable_name] + def ConvertToSql(self, expression): """Converting Logica expression into SQL.""" # print('EXPR:', expression) # Variables. if 'variable' in expression: - return self.Variable(expression['variable']) + the_type = expression.get('type', {}).get('the_type', 'Any') + return self.Variable(expression['variable'], + the_type) # Literals. if 'literal' in expression: @@ -433,7 +492,14 @@ def ConvertToSql(self, expression): if 'the_string' in literal: return self.StrLiteral(literal['the_string']) if 'the_list' in literal: - return self.ListLiteral(literal['the_list']) + the_list = literal['the_list'] + element_type = expression.get('type', {}).get('element_type_name', None) + if self.dialect.Name() == 'PostgreSQL' and element_type is None: + raise self.exception_maker(color.Format( + 'Array needs type in PostgreSQL: ' + '{warning}{the_list}{end}.', dict( + the_list=expression['expression_heritage']))) + return self.ListLiteral(the_list, element_type) if 'the_bool' in literal: return self.BoolLiteral(literal['the_bool']) if 'the_null' in literal: @@ -540,11 +606,23 @@ def ConvertToSql(self, expression): return simplified_sub # Couldn't optimize, just return the '.' expression. record = self.ConvertToSql(sub['record']) - return self.Subscript(record, subscript) + return self.Subscript(record, subscript, self.ExpressionIsTable(sub['record'])) if 'record' in expression: record = expression['record'] - return self.Record(record) + record_type = expression.get('type', {}).get('type_name', None) + if self.dialect.Name() == 'PostgreSQL' and record_type is None: + rendered_type = expression.get('type', {}).get('rendered_type', None) + raise self.exception_maker(color.Format( + 'Record needs type in PostgreSQL: ' + '{warning}{record}{end} was inferred only ' + 'an incomplete type {warning}{type}{end}.', dict( + record=expression['expression_heritage'], + type=rendered_type))) + + return self.Record( + record, + record_type=record_type) if 'combine' in expression: return '(%s)' % ( diff --git a/compiler/functors.py b/compiler/functors.py index 7e5ded87..e04a4c16 100755 --- a/compiler/functors.py +++ b/compiler/functors.py @@ -42,6 +42,16 @@ from ..compiler.dialect_libraries import recursion_library from ..parser_py import parse +import datetime + +class Timer: + def __init__(self, name): + self.name = name + self.time = datetime.datetime.now() + + def Stop(self): + print('%s / micros elapsed:' % self.name, (datetime.datetime.now() - self.time).microseconds) + class FunctorError(Exception): """Exception thrown when Make is bad.""" @@ -172,9 +182,18 @@ def BuildArgs(self, functor): while queue: e = queue.popleft() result.add(e) - for a in self.ArgsOf(e): + args_of_e = self.ArgsOf(e) + if isinstance(args_of_e, set): + arg_type = 'final' + else: + arg_type = 'preliminary' + + for a in args_of_e: if a not in result: - queue.append(a) + if arg_type == 'preliminary': + queue.append(a) + else: + result.add(a) del self.args_of[functor] diff --git a/compiler/rule_translate.py b/compiler/rule_translate.py index d2072917..cdd95541 100755 --- a/compiler/rule_translate.py +++ b/compiler/rule_translate.py @@ -206,6 +206,13 @@ def __init__(self, names_allocator=None, external_vocabulary=None, self.full_rule_text = None self.distinct_denoted = None + def SelectAsRecord(self): + return {'record': { + 'field_value': [{ + 'field': k, + 'value': {'expression': v} + } for k, v in sorted(self.select.items())]}} + def OwnVarsVocabulary(self): """Returns a map: logica variable -> SQL expression with the value.""" def TableAndFieldToSql(table, field): @@ -284,7 +291,8 @@ def ReplaceVariableEverywhere(self, u_left, u_right): ReplaceVariable(u_left, u_right, self.vars_unification) ReplaceVariable(u_left, u_right, self.constraints) - def ElliminateInternalVariables(self, assert_full_ellimination=False): + # TODO: Parameter unfold_recods just patches some bug. Careful review is needed. + def ElliminateInternalVariables(self, assert_full_ellimination=False, unfold_records=True): """Elliminates internal variables via substitution.""" variables = self.InternalVariables() while True: @@ -309,7 +317,9 @@ def ElliminateInternalVariables(self, assert_full_ellimination=False): self.ReplaceVariableEverywhere(u_left, u_right) done = False # Assignments to variables in record fields. - if True: # Confirm that unwraping works and make this unconditional. + if unfold_records: # Confirm that unwraping works and make this unconditional. + # Unwrapping goes wild sometimes. Letting it go right to left only. + # for k, r in [['left', 'right']]: for k, r in [['left', 'right'], ['right', 'left']]: if u[k] == u[r]: continue @@ -476,7 +486,11 @@ def AsSql(self, subquery_encoder=None, flag_values=None): for k, v in self.select.items(): if k == '*': - fields.append('%s.*' % ql.ConvertToSql(v)) + if 'variable' in v: + v['variable']['dont_expand'] = True # For SQLite. + fields.append( + subquery_encoder.execution.dialect.Subscript( + ql.ConvertToSql(v), '*', True)) else: fields.append('%s AS %s' % (ql.ConvertToSql(v), LogicaFieldToSqlField(k))) r += ',\n'.join(' ' + f for f in fields) @@ -504,11 +518,14 @@ def AsSql(self, subquery_encoder=None, flag_values=None): tables.append(sql) self.SortUnnestings() for element, the_list in self.unnestings: + if 'variable' in element: + # To prevent SQLite record unfolding. + element['variable']['dont_expand'] = True tables.append( subquery_encoder.execution.dialect.UnnestPhrase().format( ql.ConvertToSql(the_list), ql.ConvertToSql(element))) if not tables: - tables.append('(SELECT "singleton" as s) as unused_singleton') + tables.append("(SELECT 'singleton' as s) as unused_singleton") from_str = ', '.join(tables) # Indent the from_str. from_str = '\n'.join(' ' + l for l in from_str.split('\n')) @@ -531,7 +548,8 @@ def AsSql(self, subquery_encoder=None, flag_values=None): for v in ordered_distinct_vars) elif subquery_encoder.execution.dialect.GroupBySpecBy() == 'expr': r += ', '.join( - ql.ConvertToSql(self.select[k]) for k in ordered_distinct_vars + ql.ConvertToSqlForGroupBy(self.select[k]) + for k in ordered_distinct_vars ) else: assert False, 'Broken dialect %s, group by spec: %s' % ( @@ -609,7 +627,8 @@ def ExtractInclusionStructure(inclusion, s): 'value': { 'expression': { 'variable': { - 'var_name': var_name + 'var_name': var_name, + 'dont_expand': True } } } diff --git a/compiler/universe.py b/compiler/universe.py index fb4673ff..f29452cb 100755 --- a/compiler/universe.py +++ b/compiler/universe.py @@ -34,6 +34,7 @@ from compiler import functors from compiler import rule_translate from parser_py import parse + from type_inference.research import infer else: from ..common import color from ..compiler import dialects @@ -41,6 +42,7 @@ from ..compiler import functors from ..compiler import rule_translate from ..parser_py import parse + from ..type_inference.research import infer PredicateInfo = collections.namedtuple('PredicateInfo', ['embeddable']) @@ -135,6 +137,11 @@ class Annotations(object): def __init__(self, rules, user_flags): # Extracting DefineFlags first, so that we can use flags in @Ground # annotations. + if 'logica_default_engine' in user_flags: + self.default_engine = user_flags['logica_default_engine'] + else: + self.default_engine = 'bigquery' + self.annotations = self.ExtractAnnotations( rules, restrict_to=['@DefineFlag', '@ResetFlagValue']) self.user_flags = user_flags @@ -154,7 +161,7 @@ def Preamble(self): preamble += ( '-- Initializing PostgreSQL environment.\n' 'set client_min_messages to warning;\n' - 'create schema if not exists logica_test;\n\n') + 'create schema if not exists logica_home;\n\n') return preamble def BuildFlagValues(self): @@ -165,12 +172,14 @@ def BuildFlagValues(self): programmatic_flag_values = {} for flag, a in self.annotations['@ResetFlagValue'].items(): programmatic_flag_values[flag] = a.get('1', '${%s}' % flag) + system_flags = set(['logica_default_engine']) + allowed_flags_set = set(default_values) | system_flags - if not set(self.user_flags) <= set(default_values): + if not set(self.user_flags) <= allowed_flags_set: raise rule_translate.RuleCompileException( 'Undefined flags used: %s' % list( - set(self.user_flags) - set(default_values)), - str(set(self.user_flags) - set(default_values))) + set(self.user_flags) - allowed_flags_set), + str(set(self.user_flags) - allowed_flags_set)) flag_values = default_values flag_values.update(**programmatic_flag_values) flag_values.update(**self.user_flags) @@ -192,7 +201,6 @@ def AttachedDatabases(self): result = {} for k, v in self.annotations['@AttachDatabase'].items(): if '1' not in v: - print('>>', v) AnnotationError('@AttachDatabase must have a single argument.', v) result[k] = v['1'] @@ -245,14 +253,34 @@ def OrderBy(self, predicate_name): return FieldValuesAsList(self.annotations['@OrderBy'][predicate_name]) def Dataset(self): - return self.ExtractSingleton('@Dataset', 'logica_test') + default_dataset = 'logica_test' + # This change is intended for all engines in the future. + if self.Engine() == 'psql': + default_dataset = 'logica_home' + return self.ExtractSingleton('@Dataset', default_dataset) def Engine(self): - engine = self.ExtractSingleton('@Engine', 'bigquery') + engine = self.ExtractSingleton('@Engine', self.default_engine) if engine not in dialects.DIALECTS: AnnotationError('Unrecognized engine: %s' % engine, self.annotations['@Engine'][engine]) return engine + + def ShouldTypecheck(self): + engine = self.Engine() + + if '@Engine' not in self.annotations: + return engine == 'psql' + if len(self.annotations['@Engine'].values()) == 0: + return engine == 'psql' + + engine_annotation = list(self.annotations['@Engine'].values())[0] + if 'type_checking' not in engine_annotation: + if engine == 'psql': + return True + else: + return False + return engine_annotation['type_checking'] def ExtractSingleton(self, annotation_name, default_value): if not self.annotations[annotation_name]: @@ -394,6 +422,17 @@ def __contains__(self, key): ql.convert_to_json = True annotation = rule['head']['predicate_name'] + # Checking for aggregations. + aggregated_fields = [ + fv['field'] + for fv in rule['head']['record']['field_value'] + if 'aggregation' in fv['value']] + if aggregated_fields: + raise rule_translate.RuleCompileException( + 'Annotation may not use aggregation, but field ' + '%s is aggregated.' % ( + color.Warn(aggregated_fields[0])), + rule_text) field_values_json_str = ql.ConvertToSql( {'record': rule['head']['record']}) try: @@ -490,14 +529,20 @@ def __init__(self, rules, table_aliases=None, user_flags=None): # We need to recompute annotations, because 'Make' created more rules and # annotations. self.annotations = Annotations(extended_rules, self.user_flags) + + # Infering types if requested. + self.typing_preamble = '' + self.required_type_definitions = {} + self.predicate_signatures = {} + self.typing_engine = None + if self.annotations.ShouldTypecheck(): + self.typing_preamble = self.RunTypechecker() + # Build udfs, populating custom_udfs and custom_udf_definitions. self.BuildUdfs() # Function compilation may have added irrelevant defines: self.execution = None - if False: - self.RunTypechecker() - def UnfoldRecursion(self, rules): annotations = Annotations(rules, {}) f = functors.Functors(rules) @@ -530,7 +575,15 @@ def RunTypechecker(self): Raises: TypeInferenceError if there are any type errors. """ - inference.CheckTypes(self.rules) + rules = [r for _, r in self.rules] + typing_engine = infer.TypesInferenceEngine(rules) + typing_engine.InferTypes() + self.typing_engine = typing_engine + type_error_checker = infer.TypeErrorChecker(rules) + type_error_checker.CheckForError(mode='raise') + self.predicate_signatures = typing_engine.predicate_signature + self.required_type_definitions.update(typing_engine.collector.definitions) + return typing_engine.typing_preamble def RunMakes(self, rules): """Runs @Make instructions.""" @@ -751,6 +804,10 @@ def InitializeExecution(self, main_predicate): []) self.execution.dependencies_of = self.functors.args_of self.execution.dialect = dialects.Get(self.annotations.Engine()) + + def UpdateExecutionWithTyping(self): + if self.execution.dialect.Name() == 'PostgreSQL': + self.execution.preamble += '\n' + self.typing_preamble def FormattedPredicateSql(self, name, allocator=None): """Printing top-level formatted SQL statement with defines and exports.""" @@ -767,6 +824,8 @@ def FormattedPredicateSql(self, name, allocator=None): else: sql = self.PredicateSql(name, allocator) + self.UpdateExecutionWithTyping() + assert self.execution.workflow_predicates_stack == [name], ( 'Logica internal error: unexpected workflow stack: %s' % self.execution.workflow_predicates_stack) @@ -850,7 +909,7 @@ def RunInjections(self, s, allocator): [r] = rules rs = rule_translate.ExtractRuleStructure( r, allocator, None) - rs.ElliminateInternalVariables(assert_full_ellimination=False) + rs.ElliminateInternalVariables(assert_full_ellimination=False, unfold_records=False) new_tables.update(rs.tables) InjectStructure(s, rs) @@ -877,6 +936,11 @@ def RunInjections(self, s, allocator): } } }) + elif table_var == '*': + s.vars_unification.append({ + 'left': {'variable': {'var_name': clause_var}}, + 'right': rs.SelectAsRecord() + }) else: extra_hint = '' if table_var != '*' else ( ' Are you using .. for injectible predicate? ' @@ -915,11 +979,19 @@ def SingleRuleSql(self, rule, s = rule_translate.ExtractRuleStructure( r, allocator, external_vocabulary) - s.ElliminateInternalVariables(assert_full_ellimination=False) + # TODO(2023 July): Was this always redundant? + # s.ElliminateInternalVariables(assert_full_ellimination=False) self.RunInjections(s, allocator) s.ElliminateInternalVariables(assert_full_ellimination=True) s.UnificationsToConstraints() + type_inference = infer.TypeInferenceForStructure(s, self.predicate_signatures) + type_inference.PerformInference() + # New types may arrive here when we have an injetible predicate with variables + # which specific record type depends on the inputs. + self.required_type_definitions.update(type_inference.collector.definitions) + self.typing_preamble = infer.BuildPreamble(self.required_type_definitions) + try: sql = s.AsSql(self.MakeSubqueryTranslator(allocator), self.flag_values) except RuntimeError as runtime_error: @@ -987,8 +1059,9 @@ def TranslateTableAttachedToFile(self, table, ground, external_vocabulary): dependency_sql = self.program.UseFlagsAsParameters(dependency_sql) self.execution.workflow_predicates_stack.pop() maybe_drop_table = ( - 'DROP TABLE IF EXISTS %s;\n' % ground.table_name - if ground.overwrite else '') + 'DROP TABLE IF EXISTS %s%s;\n' % (( + ground.table_name if ground.overwrite else '', + self.execution.dialect.MaybeCascadingDeletionWord()))) export_statement = ( maybe_drop_table + 'CREATE TABLE {name} AS {dependency_sql}'.format( diff --git a/docs/index.html b/docs/index.html index 37ecd0b0..0242a23c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -329,7 +329,8 @@

Why Logica?

Logica is for engineers, data scientists and other specialists who need to perform complex data processing and analysis. Queries and pipelines written in Logica can run on -BigQuery and SQLite engines. +BigQuery, SQLite +and PostgreSQL engines. Information stored in these systems is thus available in Logica. @@ -347,11 +348,11 @@

Why Logica?

-Among other engines, there is partial support for Trino and Postgres. +Among other engines, there is partial support for Trino and Databricks. Contributions to improve this support are very welcome! diff --git a/examples/more/Simple_Postgres.ipynb b/examples/more/Simple_Postgres.ipynb new file mode 100644 index 00000000..4a96aec7 --- /dev/null +++ b/examples/more/Simple_Postgres.ipynb @@ -0,0 +1,138 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "7e89b446", + "metadata": {}, + "source": [ + "# Example of running Postgres with text Pipeline overview" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58f2b0db", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from logica import colab_logica\n", + "colab_logica.ConnectToPostgres('interactive')\n", + "colab_logica.DISPLAY_MODE = 'colab-text'" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "82c02eb6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query is stored at \u001b[1mGreeting_sql\u001b[0m variable.\n" + ] + }, + { + "data": { + "text/html": [ + "
 ▚  Greeting
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running predicate: Greeting (0 seconds)\n", + "The following table is stored at \u001b[1mGreeting\u001b[0m variable.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
col0
0Hello world!
\n", + "
" + ], + "text/plain": [ + " col0\n", + "0 Hello world!" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \n" + ] + } + ], + "source": [ + "%%logica Greeting\n", + "\n", + "@Engine(\"psql\");\n", + "\n", + "Greeting(\"Hello world!\");" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/integration_tests/psql_arg_min_max_k_test.l b/integration_tests/psql_arg_min_max_k_test.l index d0f35d77..13a54948 100755 --- a/integration_tests/psql_arg_min_max_k_test.l +++ b/integration_tests/psql_arg_min_max_k_test.l @@ -30,13 +30,8 @@ TestArgMin() ArgMin= payload -> v :- Data(v:, payload:); ArgMax3(x) = ArgMaxK(x, 3); ArgMin2(x) = ArgMinK(x, 2); -# Type is created as follows: -# create type StringIntPair as (first text, second decimal) -StringIntPair(s, i) = SqlExpr("({s}, {i})::StringIntPair", - {s:, i:}); - -TestArgMaxK() ArgMax3= StringIntPair(payload, v) -> v :- Data(v:, payload:); -TestArgMinK() ArgMin2= StringIntPair(payload, v) -> v :- Data(v:, payload:); +TestArgMaxK() ArgMax3= {payload:, v:} -> v :- Data(v:, payload:); +TestArgMinK() ArgMin2= {payload:, v:} -> v :- Data(v:, payload:); @OrderBy(Test, "arg_opt"); Test(opt: "Max", arg_opt: TestArgMax(), arg_opt_k: TestArgMaxK()); diff --git a/integration_tests/psql_argmin_list_test.l b/integration_tests/psql_argmin_list_test.l new file mode 100644 index 00000000..eecd757f --- /dev/null +++ b/integration_tests/psql_argmin_list_test.l @@ -0,0 +1,21 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +T1() ArgMin= [i] -> (i - 3) ^ 2 :- i in Range(10); +T2() ArgMax= [i, i + 1] -> (i - 3) ^ 2 :- i in Range(10); + +Test(T1(), T2()); \ No newline at end of file diff --git a/integration_tests/psql_argmin_list_test.txt b/integration_tests/psql_argmin_list_test.txt new file mode 100644 index 00000000..8dcddc7c --- /dev/null +++ b/integration_tests/psql_argmin_list_test.txt @@ -0,0 +1,5 @@ + col0 | col1 +------+-------- + {3} | {9,10} +(1 row) + diff --git a/integration_tests/psql_bool_test.l b/integration_tests/psql_bool_test.l new file mode 100644 index 00000000..9d4791e6 --- /dev/null +++ b/integration_tests/psql_bool_test.l @@ -0,0 +1,18 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +Test({a:, b:, c:}) :- a = (1 > 2), b = true, c = (a && b); \ No newline at end of file diff --git a/integration_tests/psql_combine2_test.l b/integration_tests/psql_combine2_test.l new file mode 100644 index 00000000..38bf76cb --- /dev/null +++ b/integration_tests/psql_combine2_test.l @@ -0,0 +1,21 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +T({t: 1, r: 3}); +T({t: 2, r: 4}); + +Test({a:1, dr: 2 * r}) = {b:2, c: d} :- d List= ({z: 2*i + t} :- i in Range(3)), T({t:, r:}); diff --git a/integration_tests/psql_combine2_test.txt b/integration_tests/psql_combine2_test.txt new file mode 100644 index 00000000..09bbf61b --- /dev/null +++ b/integration_tests/psql_combine2_test.txt @@ -0,0 +1,6 @@ + col0 | logica_value +-------+--------------------- + (1,6) | (2,"{(1),(3),(5)}") + (1,8) | (2,"{(2),(4),(6)}") +(2 rows) + diff --git a/integration_tests/psql_combine_test.l b/integration_tests/psql_combine_test.l new file mode 100644 index 00000000..cb3e2413 --- /dev/null +++ b/integration_tests/psql_combine_test.l @@ -0,0 +1,31 @@ +# +# Copyright 2020 Google LLC +# +# 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. + +# Testing combine expressions that could be ambigous in SQLite. +@Engine("psql"); + +T(1); +T(2); +T(3); +T(4); + +@With(R); +R(x, l) :- T(x), l == (if x == 2 || x == 3 then [x] else []); + +P1(x, y) :- T(x), y List= x; +P2(x, col1? List= y) distinct :- T(x), y in [1,2,3,4], R(x, l), ~(y in l); + +Test("test1", x, y) :- P1(x, y); +Test("test2", x, y) :- P2(x, y); \ No newline at end of file diff --git a/integration_tests/psql_combine_test.txt b/integration_tests/psql_combine_test.txt new file mode 100644 index 00000000..7f1c990e --- /dev/null +++ b/integration_tests/psql_combine_test.txt @@ -0,0 +1,12 @@ + col0 | col1 | col2 +-------+------+----------- + test1 | 1 | {1} + test1 | 2 | {2} + test1 | 3 | {3} + test1 | 4 | {4} + test2 | 1 | {1,2,3,4} + test2 | 2 | {1,3,4} + test2 | 3 | {1,2,4} + test2 | 4 | {1,2,3,4} +(8 rows) + diff --git a/integration_tests/psql_explicit_typing_test.l b/integration_tests/psql_explicit_typing_test.l new file mode 100644 index 00000000..98d2384f --- /dev/null +++ b/integration_tests/psql_explicit_typing_test.l @@ -0,0 +1,43 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +@Ground(T); +T(a: 1, b: "I"); +T(a: 2, b: "II"); +T(a: 3, b: "III"); + +@Ground(R); +R(f: {c: 5, d: [{e: "e"}]}, g: "g"); + +SaveT() += 1 :- T(); +SaveR() += 1 :- R(); + +@Ground(D, "logica_home.T"); +@Ground(RawE, "logica_home.R"); + +E(..r) :- + RawE(..r), + r ~ {f: {c: Num, d: [{e: Str}]}, g: Str}; + +@Ground(PrepareData); +PrepareData("done") += 1 :- SaveT() | SaveR(); + +Test({f:, g:}) Array= a -> {a:, b:} :- + PrepareData(), + D(a:, b:), + E(f:, g:), + a ~ Num, b ~ Str; \ No newline at end of file diff --git a/integration_tests/psql_explicit_typing_test.txt b/integration_tests/psql_explicit_typing_test.txt new file mode 100644 index 00000000..3e767415 --- /dev/null +++ b/integration_tests/psql_explicit_typing_test.txt @@ -0,0 +1,5 @@ + col0 | logica_value +---------------------+------------------------------ + ("(5,""{(e)}"")",g) | {"(1,I)","(2,II)","(3,III)"} +(1 row) + diff --git a/integration_tests/psql_game_test.l b/integration_tests/psql_game_test.l new file mode 100644 index 00000000..79823813 --- /dev/null +++ b/integration_tests/psql_game_test.l @@ -0,0 +1,73 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +# What have we learned writing this test? +# Recursive errors need improvement. +# 1. If recursion fails to eliminate, it may manifest as psql type errors. +/* +@Recursive(Advantage, 5); +BestMove(x, + target? ArgMin= y -> Advantage(y), + value? Min= Advantage(y)) distinct :- + E(x, y); + +Advantage(x) += EndgamePrize(x); +Advantage(x) += -worst_value * 0.90 :- + V(x), + BestMove(x, value: worst_value); +*/ + +# 2. Recursion of a predicate on itself and on recursive fails to eliminate. +/* +@Recursive(BestMove, 5); +BestMove(x, + target? ArgMin= y -> Advantage(y), + value? Min= Advantage(y)) distinct :- + E(x, y); + +Advantage(x) += EndgamePrize(x); +Advantage(x) += -worst_value * 0.90 :- + Advantage(Endgame()), + V(x), + BestMove(x, value: worst_value); +*/ + + +@Engine("psql"); + +E(1, 2); +E(2, 1); +E(2, 3); +E(3, 4); +E(4, 5); +V(x) distinct :- x in [a,b], E(a,b); + +Endgame() = 5; +EndgamePrize(Endgame()) = 1; + +@Recursive(BestMove, 20); +@OrderBy(BestMove, "col0"); +BestMove(x, + target? ArgMin= y -> Advantage(y), + value? Min= Advantage(y)) distinct :- + E(x, y); + +Advantage(x) += EndgamePrize(x); +Advantage(x) += 0 :- V(x); +Advantage(x) += -worst_value * 0.90 :- + V(x), + BestMove(x, value: worst_value); + +Test := BestMove(); \ No newline at end of file diff --git a/integration_tests/psql_game_test.txt b/integration_tests/psql_game_test.txt new file mode 100644 index 00000000..f9b09fd7 --- /dev/null +++ b/integration_tests/psql_game_test.txt @@ -0,0 +1,8 @@ + col0 | target | value +------+--------+-------------------------------------------- + 1 | 2 | 0.0000000000000000000000000000000000000000 + 2 | 1 | 0.0000000000000000000000000000000000000000 + 3 | 4 | -0.90 + 4 | 5 | 1 +(4 rows) + diff --git a/integration_tests/psql_pair_test.l b/integration_tests/psql_pair_test.l index f8883350..20b75d99 100644 --- a/integration_tests/psql_pair_test.l +++ b/integration_tests/psql_pair_test.l @@ -7,17 +7,13 @@ Word("water"); Word("wind"); Word("sun"); -# We assume there exists StringNumPair -# (first text, second numeric) type. -StringNumPair(s, i) = SqlExpr("({s}, {i})::StringNumPair", {s:, i:}); - -WordAndLength(StringNumPair(word, Length(word))) :- +WordAndLength({word:, length: Length(word)}) :- Word(word); -WordsByLengthList() Array= length -> StringNumPair(word, length) :- +WordsByLengthList() Array= length -> {word:, length:} :- WordAndLength(word_and_length), - word == word_and_length.first, - length == word_and_length.second; + word == word_and_length.word, + length == word_and_length.length; Test := WordsByLengthList() # If test fails create the type by running: diff --git a/integration_tests/psql_pair_test.txt b/integration_tests/psql_pair_test.txt index d279e313..c09f3aa2 100644 --- a/integration_tests/psql_pair_test.txt +++ b/integration_tests/psql_pair_test.txt @@ -1,5 +1,5 @@ logica_value ----------------------------------------------- - {"(sun,3)","(fire,4)","(wind,4)","(water,5)"} + {"(3,sun)","(4,fire)","(4,wind)","(5,water)"} (1 row) diff --git a/integration_tests/psql_purchase2_test.l b/integration_tests/psql_purchase2_test.l new file mode 100644 index 00000000..c259a2c9 --- /dev/null +++ b/integration_tests/psql_purchase2_test.l @@ -0,0 +1,76 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +Items(item: "Soap", price: 20); +Items(item: "Milk", price: 10); +Items(item: "Bread", price: 5); +Items(item: "Coffee", price: 7); +Items(item: "Firewood", price: 15); + +MoreExpensiveThan(item1) List= item2 :- + Items(item: item1, price: price1), + Items(item: item2, price: price2), + price1 > price2; + +BuyEvent(purchase_id: 1, item: "Soap", quantity: 3); +BuyEvent(purchase_id: 2, item: "Milk", quantity: 1); +BuyEvent(purchase_id: 3, item: "Bread", quantity: 2); +BuyEvent(purchase_id: 3, item: "Coffee", quantity: 1); +BuyEvent(purchase_id: 4, item: "Firewood", quantity: 5); +BuyEvent(purchase_id: 4, item: "Soap", quantity: 1); +BuyEvent(purchase_id: 5, item: "Milk", quantity: 4); +BuyEvent(purchase_id: 5, item: "Bread", quantity: 1); +BuyEvent(purchase_id: 5, item: "Coffee", quantity: 2); +BuyEvent(purchase_id: 6, item: "Firewood", quantity: 1); +BuyEvent(purchase_id: 6, item: "Soap", quantity: 3); +BuyEvent(purchase_id: 7, item: "Milk", quantity: 1); +BuyEvent(purchase_id: 7, item: "Bread", quantity: 2); +BuyEvent(purchase_id: 7, item: "Coffee", quantity: 1); +BuyEvent(purchase_id: 8, item: "Firewood", quantity: 5); +BuyEvent(purchase_id: 8, item: "Soap", quantity: 1); + +Buyer(buyer_id: 11, purchase_id: 1); +Buyer(buyer_id: 12, purchase_id: 2); +Buyer(buyer_id: 13, purchase_id: 3); +Buyer(buyer_id: 14, purchase_id: 4); +Buyer(buyer_id: 12, purchase_id: 5); +Buyer(buyer_id: 13, purchase_id: 6); +Buyer(buyer_id: 14, purchase_id: 7); +Buyer(buyer_id: 11, purchase_id: 8); + +@OrderBy(Purchase, "purchase_id"); +Purchase(purchase_id:, items:, expensive_items:, buyer_id:) :- + Buyer(buyer_id:, purchase_id:), + items List= ( + {item:, quantity:, price:} :- + BuyEvent(purchase_id:, item:, quantity:), + Items(item:, price:) + ), + expensive_items List= ( + {item:, more_expensive_than: MoreExpensiveThan(item)} :- + item_record in items, + item = item_record.item + ); + +@OrderBy(Test, "buyer_id"); +Test(buyer_id:, purchases:) :- + buyers Set= (b :- Buyer(buyer_id: b)), + buyer_id in buyers, + purchases List= ( + {purchase: items} :- + Purchase(items:, buyer_id:) + ); diff --git a/integration_tests/psql_purchase2_test.txt b/integration_tests/psql_purchase2_test.txt new file mode 100644 index 00000000..0397f5b8 --- /dev/null +++ b/integration_tests/psql_purchase2_test.txt @@ -0,0 +1,8 @@ + buyer_id | purchases +----------+-------------------------------------------------------------------------------------------------------------------------------- + 11 | {"(\"{\"\"(Soap,20,3)\"\"}\")","(\"{\"\"(Soap,20,1)\"\",\"\"(Firewood,15,5)\"\"}\")"} + 12 | {"(\"{\"\"(Milk,10,1)\"\"}\")","(\"{\"\"(Milk,10,4)\"\",\"\"(Bread,5,1)\"\",\"\"(Coffee,7,2)\"\"}\")"} + 13 | {"(\"{\"\"(Bread,5,2)\"\",\"\"(Coffee,7,1)\"\"}\")","(\"{\"\"(Soap,20,3)\"\",\"\"(Firewood,15,1)\"\"}\")"} + 14 | {"(\"{\"\"(Soap,20,1)\"\",\"\"(Firewood,15,5)\"\"}\")","(\"{\"\"(Milk,10,1)\"\",\"\"(Bread,5,2)\"\",\"\"(Coffee,7,1)\"\"}\")"} +(4 rows) + diff --git a/integration_tests/psql_purchase3_test.l b/integration_tests/psql_purchase3_test.l new file mode 100644 index 00000000..213a5387 --- /dev/null +++ b/integration_tests/psql_purchase3_test.l @@ -0,0 +1,69 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +Items(item: "Soap", price: 20); +Items(item: "Milk", price: 10); +Items(item: "Bread", price: 5); +Items(item: "Coffee", price: 7); +Items(item: "Firewood", price: 15); + +MoreExpensiveThan(item1) List= item2 :- + Items(item: item1, price: price1), + Items(item: item2, price: price2), + price1 > price2; + +BuyEvent(purchase_id: 1, item: "Soap", quantity: 3); +BuyEvent(purchase_id: 2, item: "Milk", quantity: 1); +BuyEvent(purchase_id: 3, item: "Bread", quantity: 2); +BuyEvent(purchase_id: 3, item: "Coffee", quantity: 1); +BuyEvent(purchase_id: 4, item: "Firewood", quantity: 5); +BuyEvent(purchase_id: 4, item: "Soap", quantity: 1); +BuyEvent(purchase_id: 5, item: "Milk", quantity: 4); +BuyEvent(purchase_id: 5, item: "Bread", quantity: 1); +BuyEvent(purchase_id: 5, item: "Coffee", quantity: 2); +BuyEvent(purchase_id: 6, item: "Firewood", quantity: 1); +BuyEvent(purchase_id: 6, item: "Soap", quantity: 3); +BuyEvent(purchase_id: 7, item: "Milk", quantity: 1); +BuyEvent(purchase_id: 7, item: "Bread", quantity: 2); +BuyEvent(purchase_id: 7, item: "Coffee", quantity: 1); +BuyEvent(purchase_id: 8, item: "Firewood", quantity: 5); +BuyEvent(purchase_id: 8, item: "Soap", quantity: 1); + +Buyer(buyer_id: 11, purchase_id: 1); +Buyer(buyer_id: 12, purchase_id: 2); +Buyer(buyer_id: 13, purchase_id: 3); +Buyer(buyer_id: 14, purchase_id: 4); +Buyer(buyer_id: 12, purchase_id: 5); +Buyer(buyer_id: 13, purchase_id: 6); +Buyer(buyer_id: 14, purchase_id: 7); +Buyer(buyer_id: 11, purchase_id: 8); + +@OrderBy(Purchase, "purchase_id"); +Purchase(purchase_id:, items? List= x, buyer_id:) distinct :- + Buyer(buyer_id:, purchase_id:), + x = {item: item_name, quantity:, price:}, + BuyEvent(purchase_id:, item: item_name, quantity:), + Items(item: item_name, price:); + +@OrderBy(Test, "buyer_id"); +Test(buyer_id:, purchases:) :- + buyers Set= (b :- Buyer(buyer_id: b)), + buyer_id in buyers, + purchases List= ( + {purchase: items} :- + Purchase(items:, buyer_id:) + ); diff --git a/integration_tests/psql_purchase3_test.txt b/integration_tests/psql_purchase3_test.txt new file mode 100644 index 00000000..0397f5b8 --- /dev/null +++ b/integration_tests/psql_purchase3_test.txt @@ -0,0 +1,8 @@ + buyer_id | purchases +----------+-------------------------------------------------------------------------------------------------------------------------------- + 11 | {"(\"{\"\"(Soap,20,3)\"\"}\")","(\"{\"\"(Soap,20,1)\"\",\"\"(Firewood,15,5)\"\"}\")"} + 12 | {"(\"{\"\"(Milk,10,1)\"\"}\")","(\"{\"\"(Milk,10,4)\"\",\"\"(Bread,5,1)\"\",\"\"(Coffee,7,2)\"\"}\")"} + 13 | {"(\"{\"\"(Bread,5,2)\"\",\"\"(Coffee,7,1)\"\"}\")","(\"{\"\"(Soap,20,3)\"\",\"\"(Firewood,15,1)\"\"}\")"} + 14 | {"(\"{\"\"(Soap,20,1)\"\",\"\"(Firewood,15,5)\"\"}\")","(\"{\"\"(Milk,10,1)\"\",\"\"(Bread,5,2)\"\",\"\"(Coffee,7,1)\"\"}\")"} +(4 rows) + diff --git a/integration_tests/psql_purchase_test.l b/integration_tests/psql_purchase_test.l new file mode 100644 index 00000000..f0585e4c --- /dev/null +++ b/integration_tests/psql_purchase_test.l @@ -0,0 +1,69 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +Items(item: "Soap", price: 20); +Items(item: "Milk", price: 10); +Items(item: "Bread", price: 5); +Items(item: "Coffee", price: 7); +Items(item: "Firewood", price: 15); + +MoreExpensiveThan(item1) List= item2 :- + Items(item: item1, price: price1), + Items(item: item2, price: price2), +price1 > price2; + +BuyEvent(purchase_id: 1, item: "Soap", quantity: 3); +BuyEvent(purchase_id: 2, item: "Milk", quantity: 1); +BuyEvent(purchase_id: 3, item: "Bread", quantity: 2); +BuyEvent(purchase_id: 3, item: "Coffee", quantity: 1); +BuyEvent(purchase_id: 4, item: "Firewood", quantity: 5); +BuyEvent(purchase_id: 4, item: "Soap", quantity: 1); +BuyEvent(purchase_id: 5, item: "Milk", quantity: 4); +BuyEvent(purchase_id: 5, item: "Bread", quantity: 1); +BuyEvent(purchase_id: 5, item: "Coffee", quantity: 2); +BuyEvent(purchase_id: 6, item: "Firewood", quantity: 1); +BuyEvent(purchase_id: 6, item: "Soap", quantity: 3); +BuyEvent(purchase_id: 7, item: "Milk", quantity: 1); +BuyEvent(purchase_id: 7, item: "Bread", quantity: 2); +BuyEvent(purchase_id: 7, item: "Coffee", quantity: 1); +BuyEvent(purchase_id: 8, item: "Firewood", quantity: 5); +BuyEvent(purchase_id: 8, item: "Soap", quantity: 1); + +Buyer(buyer_id: 11, purchase_id: 1); +Buyer(buyer_id: 12, purchase_id: 2); +Buyer(buyer_id: 13, purchase_id: 3); +Buyer(buyer_id: 14, purchase_id: 4); +Buyer(buyer_id: 12, purchase_id: 5); +Buyer(buyer_id: 13, purchase_id: 6); +Buyer(buyer_id: 14, purchase_id: 7); +Buyer(buyer_id: 11, purchase_id: 8); + +@OrderBy(Purchase, "purchase_id"); +Purchase(purchase_id:, items:, expensive_items:, buyer_id:) :- + Buyer(buyer_id:, purchase_id:), + items List= ( + {item:, quantity:, price:} :- + BuyEvent(purchase_id:, item:, quantity:), + Items(item:, price:) + ), + expensive_items List= ( + {item:, more_expensive_than: MoreExpensiveThan(item)} :- + item_record in items, + item = item_record.item + ); + +Test(..r) :- Purchase(..r); diff --git a/integration_tests/psql_purchase_test.txt b/integration_tests/psql_purchase_test.txt new file mode 100644 index 00000000..8eb80ded --- /dev/null +++ b/integration_tests/psql_purchase_test.txt @@ -0,0 +1,11 @@ ++----------------------------------------------+----------------------------------------------------------------------------------+---------- + 1 | {"(Soap,20,3)"} | {"(Soap,\"{Milk,Bread,Coffee,Firewood}\")"} | 11 + 2 | {"(Milk,10,1)"} | {"(Milk,\"{Bread,Coffee}\")"} | 12 + 3 | {"(Bread,5,2)","(Coffee,7,1)"} | {"(Coffee,{Bread})"} | 13 + 4 | {"(Soap,20,1)","(Firewood,15,5)"} | {"(Soap,\"{Milk,Bread,Coffee,Firewood}\")","(Firewood,\"{Milk,Bread,Coffee}\")"} | 14 + 5 | {"(Milk,10,4)","(Bread,5,1)","(Coffee,7,2)"} | {"(Milk,\"{Bread,Coffee}\")","(Coffee,{Bread})"} | 12 + 6 | {"(Soap,20,3)","(Firewood,15,1)"} | {"(Soap,\"{Milk,Bread,Coffee,Firewood}\")","(Firewood,\"{Milk,Bread,Coffee}\")"} | 13 + 7 | {"(Milk,10,1)","(Bread,5,2)","(Coffee,7,1)"} | {"(Milk,\"{Bread,Coffee}\")","(Coffee,{Bread})"} | 14 + 8 | {"(Soap,20,1)","(Firewood,15,5)"} | {"(Soap,\"{Milk,Bread,Coffee,Firewood}\")","(Firewood,\"{Milk,Bread,Coffee}\")"} | 11 +(8 rows) + diff --git a/integration_tests/psql_record_combine_test.l b/integration_tests/psql_record_combine_test.l new file mode 100644 index 00000000..f49c7525 --- /dev/null +++ b/integration_tests/psql_record_combine_test.l @@ -0,0 +1,21 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +T(1); + +Test(x, {o:1}) :- x List= ({a:} :- a in [{z:}]), T(z); + diff --git a/integration_tests/psql_record_combine_test.txt b/integration_tests/psql_record_combine_test.txt new file mode 100644 index 00000000..6014ba8b --- /dev/null +++ b/integration_tests/psql_record_combine_test.txt @@ -0,0 +1,5 @@ + col0 | col1 +---------------+------ + {"(\"(1)\")"} | (1) +(1 row) + diff --git a/integration_tests/psql_recursion_test.l b/integration_tests/psql_recursion_test.l index 211d0524..e5c365fc 100644 --- a/integration_tests/psql_recursion_test.l +++ b/integration_tests/psql_recursion_test.l @@ -27,7 +27,7 @@ ComponentOf(x) Min= y :- Distance(x, y); @OrderBy(Analyzed, "vertex"); Analyzed(vertex: x, component: ComponentOf(x), - distances? Array= (-y) -> [y, Distance(x, y)]) distinct; + distances? Array= (-y) -> {y:, d: Distance(x, y)}) distinct; Test := Analyzed(); diff --git a/integration_tests/psql_recursion_test.txt b/integration_tests/psql_recursion_test.txt index 41a1b056..3dad4331 100644 --- a/integration_tests/psql_recursion_test.txt +++ b/integration_tests/psql_recursion_test.txt @@ -1,15 +1,15 @@ - vertex | component | distances ---------+-----------+--------------------------------- - 1 | 1 | {{5,4},{4,3},{3,2},{2,1},{1,0}} - 2 | 1 | {{5,3},{4,2},{3,1},{2,0},{1,1}} - 3 | 1 | {{5,2},{4,1},{3,0},{2,1},{1,2}} - 4 | 1 | {{5,1},{4,0},{3,1},{2,2},{1,3}} - 5 | 1 | {{5,0},{4,1},{3,2},{2,3},{1,4}} - 6 | 6 | {{8,2},{7,1},{6,0}} - 7 | 6 | {{8,1},{7,0},{6,1}} - 8 | 6 | {{8,0},{7,1},{6,2}} - 9 | 9 | {{11,1},{10,1},{9,0}} - 10 | 9 | {{11,1},{10,0},{9,1}} - 11 | 9 | {{11,0},{10,1},{9,1}} + vertex | component | distances +--------+-----------+------------------------------------------- + 1 | 1 | {"(4,5)","(3,4)","(2,3)","(1,2)","(0,1)"} + 2 | 1 | {"(3,5)","(2,4)","(1,3)","(0,2)","(1,1)"} + 3 | 1 | {"(2,5)","(1,4)","(0,3)","(1,2)","(2,1)"} + 4 | 1 | {"(1,5)","(0,4)","(1,3)","(2,2)","(3,1)"} + 5 | 1 | {"(0,5)","(1,4)","(2,3)","(3,2)","(4,1)"} + 6 | 6 | {"(2,8)","(1,7)","(0,6)"} + 7 | 6 | {"(1,8)","(0,7)","(1,6)"} + 8 | 6 | {"(0,8)","(1,7)","(2,6)"} + 9 | 9 | {"(1,11)","(1,10)","(0,9)"} + 10 | 9 | {"(1,11)","(0,10)","(1,9)"} + 11 | 9 | {"(0,11)","(1,10)","(1,9)"} (11 rows) diff --git a/integration_tests/psql_simple_structs_test.l b/integration_tests/psql_simple_structs_test.l new file mode 100644 index 00000000..ab9db4dd --- /dev/null +++ b/integration_tests/psql_simple_structs_test.l @@ -0,0 +1,24 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +Test(x -> y, + {a: x, b: {c: [x, y]}, c: z}, + {a: x, b: {c: x, d: y}, some_field: x + y}) :- + x in [1, 2], + y in [3, 4], + z in [{a: "abc"}, {a: "def"}]; + diff --git a/integration_tests/psql_simple_structs_test.txt b/integration_tests/psql_simple_structs_test.txt new file mode 100644 index 00000000..6426f970 --- /dev/null +++ b/integration_tests/psql_simple_structs_test.txt @@ -0,0 +1,12 @@ + col0 | col1 | col2 +-------+---------------------------+--------------- + (1,3) | (1,"(""{1,3}"")","(abc)") | (1,"(1,3)",4) + (1,4) | (1,"(""{1,4}"")","(abc)") | (1,"(1,4)",5) + (2,3) | (2,"(""{2,3}"")","(abc)") | (2,"(2,3)",5) + (2,4) | (2,"(""{2,4}"")","(abc)") | (2,"(2,4)",6) + (1,3) | (1,"(""{1,3}"")","(def)") | (1,"(1,3)",4) + (1,4) | (1,"(""{1,4}"")","(def)") | (1,"(1,4)",5) + (2,3) | (2,"(""{2,3}"")","(def)") | (2,"(2,3)",5) + (2,4) | (2,"(""{2,4}"")","(def)") | (2,"(2,4)",6) +(8 rows) + diff --git a/integration_tests/psql_structs_ground_test.l b/integration_tests/psql_structs_ground_test.l new file mode 100644 index 00000000..2f1da497 --- /dev/null +++ b/integration_tests/psql_structs_ground_test.l @@ -0,0 +1,27 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +@Ground(Goal); +Goal("human" -> "happiness"); +Goal("bird" -> "flight"); +Goal("machine" -> "?"); +Goal("universe" -> "?"); + +@OrderBy(Test, "col0"); +Test(x) :- Goal(x -> "?"); + + diff --git a/integration_tests/psql_structs_ground_test.txt b/integration_tests/psql_structs_ground_test.txt new file mode 100644 index 00000000..a25d5026 --- /dev/null +++ b/integration_tests/psql_structs_ground_test.txt @@ -0,0 +1,6 @@ + col0 +---------- + machine + universe +(2 rows) + diff --git a/integration_tests/run_tests.py b/integration_tests/run_tests.py index 66b7c2c5..75c869f5 100755 --- a/integration_tests/run_tests.py +++ b/integration_tests/run_tests.py @@ -69,6 +69,8 @@ def RunAll(test_presto=False, test_trino=False): RunTest("rec_small_cycle_test") RunTest("rec_cycle_test") + RunTest("sqlite_shortest_path_test") + RunTest("sqlite_records_test") RunTest("sqlite_is_test") RunTest("sqlite_record_assembler") RunTest("sqlite_assignment_test") @@ -89,6 +91,17 @@ def RunAll(test_presto=False, test_trino=False): RunTest("sqlite_reachability") RunTest("sqlite_element_test") + RunTest("psql_game_test") + RunTest("psql_explicit_typing_test") + RunTest("psql_argmin_list_test") + RunTest("psql_purchase_test") + RunTest("psql_purchase2_test") + RunTest("psql_purchase3_test") + RunTest("psql_combine_test") + RunTest("psql_combine2_test") + RunTest("psql_record_combine_test") + RunTest("psql_structs_ground_test") + RunTest("psql_simple_structs_test") RunTest("psql_recursion_test") RunTest("psql_test") RunTest("psql_arg_min_test") diff --git a/integration_tests/sqlite_array_sub_test.l b/integration_tests/sqlite_array_sub_test.l index c917acf4..06425b7f 100644 --- a/integration_tests/sqlite_array_sub_test.l +++ b/integration_tests/sqlite_array_sub_test.l @@ -17,7 +17,7 @@ # Please never use it this way. # This feature is provided for Spanner indexes specification. -@Engine("sqlite"); +@Engine("sqlite", type_checking: false); Books() = [ { diff --git a/integration_tests/sqlite_funcs_test.l b/integration_tests/sqlite_funcs_test.l index 0c5f06d8..ad888634 100644 --- a/integration_tests/sqlite_funcs_test.l +++ b/integration_tests/sqlite_funcs_test.l @@ -15,7 +15,7 @@ # Testing misc functions of SQLite. -@Engine("sqlite"); +@Engine("sqlite", type_checking: false); Test("Set") = Size(s) :- s Set= (x + y :- x in Range(5), y in Range(5)); Test("Sort") = Sort([6,3,2,5,1,4]); diff --git a/integration_tests/sqlite_functors_test.l b/integration_tests/sqlite_functors_test.l index 38797638..dcd07c08 100644 --- a/integration_tests/sqlite_functors_test.l +++ b/integration_tests/sqlite_functors_test.l @@ -16,7 +16,7 @@ # Testing subtle functor application order. @Engine("sqlite"); -T1("t1"); +T1() = "t1"; F() = T1(); D := F(); B() = D(); diff --git a/integration_tests/sqlite_record_assembler.l b/integration_tests/sqlite_record_assembler.l index 0a1b2988..f7842eb4 100644 --- a/integration_tests/sqlite_record_assembler.l +++ b/integration_tests/sqlite_record_assembler.l @@ -15,7 +15,7 @@ # Testing SQLite record introspection. -@Engine("sqlite"); +@Engine("sqlite", type_checking: false); Test(1, AssembleRecord(["planet" -> "Earth", "species" -> "Humans"])); Test(2, DisassembleRecord({planet: "Mars", species: "Apes"})); diff --git a/integration_tests/sqlite_records_test.l b/integration_tests/sqlite_records_test.l new file mode 100644 index 00000000..c71e89e4 --- /dev/null +++ b/integration_tests/sqlite_records_test.l @@ -0,0 +1,26 @@ +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("sqlite", type_checking: true); + +T(a: 1, b: 2); +T(a: 3, b: 4); + +D(..r) :- T(..r), r.a == 1; + +E(two_a: 2 * a) :- T(a:); + +@OrderBy(Test, "a", "ta"); +Test(a: r.a, b: r.b, r:, c:, two_a_record:, ta: two_a_record.two_a) :- D(a: c), T(..r), E(..two_a_record); \ No newline at end of file diff --git a/integration_tests/sqlite_records_test.txt b/integration_tests/sqlite_records_test.txt new file mode 100644 index 00000000..944f5419 --- /dev/null +++ b/integration_tests/sqlite_records_test.txt @@ -0,0 +1,8 @@ ++---+---+---------------+---+--------------+----+ +| a | b | r | c | two_a_record | ta | ++---+---+---------------+---+--------------+----+ +| 1 | 2 | {"a":1,"b":2} | 1 | {"two_a":2} | 2 | +| 1 | 2 | {"a":1,"b":2} | 1 | {"two_a":6} | 6 | +| 3 | 4 | {"a":3,"b":4} | 1 | {"two_a":2} | 2 | +| 3 | 4 | {"a":3,"b":4} | 1 | {"two_a":6} | 6 | ++---+---+---------------+---+--------------+----+ \ No newline at end of file diff --git a/integration_tests/sqlite_shortest_path_test.l b/integration_tests/sqlite_shortest_path_test.l new file mode 100644 index 00000000..fc71bd44 --- /dev/null +++ b/integration_tests/sqlite_shortest_path_test.l @@ -0,0 +1,38 @@ +# +# Copyright 2020 Google LLC +# +# 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. + +# This example was causing record unfolding go wild. + +@Engine("sqlite"); + +Edge("a", "b"); +Edge("b", "c"); +Edge("c", "d"); +Edge("d", "e"); +Edge("d", "a"); + +ShortestPath(open_path) = ArgMin(k -> v).nodes :- + k == {nodes: open_path}, + v == Size(open_path); + +OpenPath(start, end) ShortestPath= [start] :- + (Edge(start, end) | Edge(end, start)); + +OpenPath(start, end) ShortestPath= ArrayConcat(path_one, path_two) :- + OpenPath(start, intermediate_node) = path_one, + OpenPath(intermediate_node, end) = path_two, + start != end; + +Test(a, b, OpenPath(a, b)); \ No newline at end of file diff --git a/integration_tests/sqlite_shortest_path_test.txt b/integration_tests/sqlite_shortest_path_test.txt new file mode 100644 index 00000000..a50de50d --- /dev/null +++ b/integration_tests/sqlite_shortest_path_test.txt @@ -0,0 +1,24 @@ ++------+------+-----------------+ +| col0 | col1 | col2 | ++------+------+-----------------+ +| a | b | ["a"] | +| a | c | ["a", "b"] | +| a | d | ["a"] | +| a | e | ["a", "d"] | +| b | a | ["b"] | +| b | c | ["b"] | +| b | d | ["b", "a"] | +| b | e | ["b", "a", "d"] | +| c | a | ["c", "b"] | +| c | b | ["c"] | +| c | d | ["c"] | +| c | e | ["c", "d"] | +| d | a | ["d"] | +| d | b | ["d", "a"] | +| d | c | ["d"] | +| d | e | ["d"] | +| e | a | ["e", "d"] | +| e | b | ["e", "d", "a"] | +| e | c | ["e", "d"] | +| e | d | ["e"] | ++------+------+-----------------+ \ No newline at end of file diff --git a/integration_tests/sqlite_unwrapping_test.l b/integration_tests/sqlite_unwrapping_test.l index f6b05d74..3af658a7 100644 --- a/integration_tests/sqlite_unwrapping_test.l +++ b/integration_tests/sqlite_unwrapping_test.l @@ -13,7 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -@Engine("sqlite"); +@Engine("sqlite", type_checking: false); + Test("simple_assignment", {a:, b:}) :- {a:, b:} == {a: "va", b: "vb"}; Test("arrow_assignment", {a:, b:}) :- a -> b == "va" -> "vb"; diff --git a/logica.py b/logica.py index b9c5262c..0acb6fc1 100755 --- a/logica.py +++ b/logica.py @@ -47,6 +47,7 @@ from compiler import rule_translate from compiler import universe from parser_py import parse + from type_inference.research import infer else: from .common import color from .common import sqlite3_logica @@ -54,6 +55,7 @@ from .compiler import rule_translate from .compiler import universe from .parser_py import parse + from .type_inference.research import infer def ReadUserFlags(rules, argv): @@ -130,13 +132,14 @@ def main(argv): 'GoodIdea(snack: "carrots")\'') return 1 - if len(argv) == 3 and argv[2] == 'parse': + if len(argv) == 3 and argv[2] in ['parse', 'infer_types', 'show_signatures']: pass # compile needs just 2 actual arguments. else: if len(argv) < 4: print('Not enough arguments. Run \'logica help\' for help.', file=sys.stderr) return 1 + predicates = argv[3] if argv[1] == '-': filename = '/dev/stdin' @@ -145,7 +148,8 @@ def main(argv): command = argv[2] - commands = ['parse', 'print', 'run', 'run_to_csv', 'run_in_terminal'] + commands = ['parse', 'print', 'run', 'run_to_csv', 'run_in_terminal', + 'infer_types', 'show_signatures'] if command not in commands: print(color.Format('Unknown command {warning}{command}{end}. ' @@ -155,6 +159,13 @@ def main(argv): if not os.path.exists(filename): print('File not found: %s' % filename, file=sys.stderr) return 1 + + if command == 'run_in_terminal': + from tools import run_in_terminal + artistic_table = run_in_terminal.Run(filename, predicates) + print(artistic_table) + return + program_text = open(filename).read() try: @@ -165,11 +176,28 @@ def main(argv): sys.exit(1) if command == 'parse': - # No indentation to avoid file size inflation. - print(json.dumps(parsed_rules, sort_keys=True, indent='')) + # Minimal indentation for better readability of deep objects. + print(json.dumps(parsed_rules, sort_keys=True, indent=' ')) return 0 - predicates = argv[3] + if command == 'infer_types': + typing_engine = infer.TypesInferenceEngine(parsed_rules) + typing_engine.InferTypes() + # print(parsed_rules) + print(json.dumps(parsed_rules, sort_keys=True, indent=' ')) + return 0 + + if command == 'show_signatures': + try: + logic_program = universe.LogicaProgram(parsed_rules) + if not logic_program.typing_engine: + logic_program.RunTypechecker() + except infer.TypeErrorCaughtException as type_error_exception: + print(logic_program.typing_engine.ShowPredicateTypes()) + type_error_exception.ShowMessage() + return 1 + print(logic_program.typing_engine.ShowPredicateTypes()) + return 0 user_flags = ReadUserFlags(parsed_rules, argv[4:]) @@ -188,6 +216,10 @@ def main(argv): except functors.FunctorError as functor_exception: functor_exception.ShowMessage() sys.exit(1) + except infer.TypeErrorCaughtException as type_error_exception: + type_error_exception.ShowMessage() + sys.exit(1) + if command == 'print': print(formatted_sql) @@ -243,11 +275,6 @@ def main(argv): assert False, 'Unknown engine: %s' % engine print(o.decode()) - if command == 'run_in_terminal': - from tools import run_in_terminal - artistic_table = run_in_terminal.Run(filename, predicate) - print(artistic_table) - def run_main(): """Run main function with system arguments.""" diff --git a/parser_py/parse.py b/parser_py/parse.py index 027496f3..5806280c 100755 --- a/parser_py/parse.py +++ b/parser_py/parse.py @@ -83,6 +83,14 @@ def Pieces(self): self.heritage[self.start:self.stop], self.heritage[self.stop:]) + def Display(self): + a, b, c = self.Pieces() + if not b: + b = '' + return color.Format( + '{before}{warning}{error_text}{end}{after}', + dict(before=a, error_text=b, after=c)) + class ParsingException(Exception): """Exception thrown by parsing.""" @@ -571,7 +579,7 @@ def ParseLiteral(s): def ParseInfix(s, operators=None): """Parses an infix operator expression.""" operators = operators or [ - '||', '&&', '->', '==', '<=', '>=', '<', '>', '!=', '=', + '||', '&&', '->', '==', '<=', '>=', '<', '>', '!=', '=', '~', ' in ', ' is not ', ' is ', '++?', '++', '+', '-', '*', '/', '%', '^', '!'] unary_operators = ['-', '!'] @@ -590,6 +598,8 @@ def ParseInfix(s, operators=None): 'predicate_name': op, 'record': ParseRecordInternals(right) } + if op == '~' and not left: + return None # Negation is special. left_expr = ParseExpression(left) right_expr = ParseExpression(right) @@ -716,6 +726,11 @@ def ParseImplication(s): def ParseExpression(s): + e = ActuallyParseExpression(s) + e['expression_heritage'] = s + return e + +def ActuallyParseExpression(s): """Parsing logica.Expression.""" v = ParseCombine(s) if v: @@ -794,6 +809,9 @@ def ParseGenericCall(s, opening, closing): # Specialcasing `=` assignment operator for definition. if predicate == '`=`': predicate = '=' + # Specialcasing `~` type equality operator for definition. + if predicate == '`~`': + predicate = '~' return predicate, s[idx + 1: -1] diff --git a/run_all_tests.py b/run_all_tests.py index d51462f8..76a73460 100755 --- a/run_all_tests.py +++ b/run_all_tests.py @@ -21,6 +21,7 @@ from common import logica_test from integration_tests import run_tests as integration_tests from integration_tests.import_tests import run_tests as import_tests +from type_inference.research.integration_tests import run_tests as type_inference_tests if 'golden_run' in sys.argv: @@ -35,5 +36,6 @@ logica_test.PrintHeader() +type_inference_tests.RunAll() integration_tests.RunAll() import_tests.RunAll() diff --git a/tools/run_in_terminal.py b/tools/run_in_terminal.py index fe3ee985..5dfba444 100644 --- a/tools/run_in_terminal.py +++ b/tools/run_in_terminal.py @@ -16,22 +16,33 @@ # Utility to run pipeline in terminal with ASCII art showing progress. +import json +import os +import sys + if not __package__ or '.' not in __package__: from common import concertina_lib from compiler import universe from parser_py import parse + from common import psql_logica from common import sqlite3_logica + from compiler import functors + from compiler import rule_translate + from type_inference.research import infer else: from ..common import concertina_lib from ..compiler import universe from ..parser_py import parse + from ..common import psql_logica from ..common import sqlite3_logica - + from ..compiler import functors + from ..compiler import rule_translate + from ..type_inference.research import infer class SqlRunner(object): def __init__(self, engine): self.engine = engine - assert engine in ['sqlite', 'bigquery'] + assert engine in ['sqlite', 'bigquery', 'psql'] if engine == 'sqlite': self.connection = sqlite3_logica.SqliteConnect() else: @@ -45,6 +56,9 @@ def __init__(self, engine): credentials, project = auth.default() else: credentials, project = None, None + if engine == 'psql': + self.connection = psql_logica.ConnectToPostgres('environment') + self.bq_credentials = credentials self.bq_project = project @@ -66,12 +80,13 @@ def RunSQL(sql, engine, connection=None, is_final=False, # pandas.read_gbq(sql, project_id=bq_project_id) return list(df.columns), [list(r) for _, r in df.iterrows()] elif engine == 'psql': - import pandas if is_final: - df = pandas.read_sql(sql, connection) - return list(df.columns), [list(r) for _, r in df.iterrows()] + cursor = psql_logica.PostgresExecute(sql, connection) + rows = [list(map(psql_logica.DigestPsqlType, row)) + for row in cursor.fetchall()] + return [d[0] for d in cursor.description], rows else: - return connection.execute(sql) + psql_logica.PostgresExecute(sql, connection) elif engine == 'sqlite': try: if is_final: @@ -92,16 +107,32 @@ def RunSQL(sql, engine, connection=None, is_final=False, def Run(filename, predicate_name): - rules = parse.ParseFile(open(filename).read())['rule'] - program = universe.LogicaProgram(rules) - engine = program.annotations.Engine() + try: + rules = parse.ParseFile(open(filename).read())['rule'] + except parse.ParsingException as parsing_exception: + parsing_exception.ShowMessage() + sys.exit(1) + + + try: + program = universe.LogicaProgram(rules) + engine = program.annotations.Engine() - # This is needed to build the program execution. - unused_sql = program.FormattedPredicateSql(predicate_name) + # This is needed to build the program execution. + unused_sql = program.FormattedPredicateSql(predicate_name) - (header, rows) = concertina_lib.ExecuteLogicaProgram( - [program.execution], SqlRunner(engine), engine, - display_mode='terminal')[predicate_name] + (header, rows) = concertina_lib.ExecuteLogicaProgram( + [program.execution], SqlRunner(engine), engine, + display_mode='terminal')[predicate_name] + except rule_translate.RuleCompileException as rule_compilation_exception: + rule_compilation_exception.ShowMessage() + sys.exit(1) + except functors.FunctorError as functor_exception: + functor_exception.ShowMessage() + sys.exit(1) + except infer.TypeErrorCaughtException as type_error_exception: + type_error_exception.ShowMessage() + sys.exit(1) artistic_table = sqlite3_logica.ArtisticTable(header, rows) return artistic_table diff --git a/type_inference/research/algebra.py b/type_inference/research/algebra.py new file mode 100644 index 00000000..de8f1d4b --- /dev/null +++ b/type_inference/research/algebra.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + + +class OpenRecord(dict): + def __str__(self): + return '{%s, ...}' % str(dict(self))[1:-1] + def __repr__(self): + return str(self) + + +class ClosedRecord(dict): + def __str__(self): + return str(dict(self)) + def __repr__(self): + return str(self) + + +class BadType(tuple): + def __str__(self): + return f'({self[0]} is incompatible with {self[1]})' + def __repr__(self): + return str(self) + + +def Rank(x): + """Rank of the type, arbitrary order for sorting.""" + if isinstance(x, BadType): # Tuple means error. + return -1 + if x == 'Any': + return 0 + if x == 'Num': + return 1 + if x == 'Str': + return 2 + if isinstance(x, list): + return 3 + if isinstance(x, OpenRecord): + return 4 + if isinstance(x, ClosedRecord): + return 5 + assert False, 'Bad type: %s' % x + + +def Incompatible(a, b): + return BadType((a, b)) + + +def Intersect(a, b): + """Intersection of types a and b.""" + if isinstance(a, BadType) or isinstance(b, BadType): + return a + + if Rank(a) > Rank(b): + a, b = b, a + + if a == 'Any': + return b + + if a in ('Num', 'Str'): + if a == b: + return b + return Incompatible(a, b) # Type error: a is incompatible with b. + + if isinstance(a, list): + if isinstance(b, list): + a_element, b_element = a + b + new_element = Intersect(a_element, b_element) + if isinstance(new_element, BadType): + return Incompatible(a, b) + return [new_element] + return Incompatible(a, b) + + if isinstance(a, OpenRecord): + if isinstance(b, OpenRecord): + return IntersectFriendlyRecords(a, b, OpenRecord) + if isinstance(b, ClosedRecord): + if set(a) <= set(b): + return IntersectFriendlyRecords(a, b, ClosedRecord) + return Incompatible(a, b) + assert False + + if isinstance(a, ClosedRecord): + if isinstance(b, ClosedRecord): + if set(a) == set(b): + return IntersectFriendlyRecords(a, b, ClosedRecord) + return Incompatible(a, b) + assert False + assert False + +def IntersectFriendlyRecords(a, b, record_type): + """Intersecting records assuming that their fields are compatible.""" + result = {} + for f in set(a) | set(b): + x = Intersect(a.get(f, 'Any'), b.get(f, 'Any')) + if isinstance(x, BadType): # Ooops, field type error. + return Incompatible(a, b) + result[f] = x + return record_type(result) + +def IntersectListElement(a_list, b_element): + """Analysis of expression `b in a`.""" + a_result = Intersect(a_list, [b_element]) + if isinstance(a_result, BadType): + return (Incompatible(a_list, [b_element]), b_element) + + if isinstance(b_element, list): # No lists of lists in BigQuery. + return (a_result, Incompatible(b_element, 'NotAList')) + + [a_element] = a_result + b_result = Intersect(b_element, a_element) + return (a_result, b_result) + +def IntersectRecordField(a_record, field_name, b_field_value): + """Analysis of expresson `a.f = b`.""" + a_result = Intersect(a_record, OpenRecord({field_name: b_field_value})) + if isinstance(a_result, BadType): + return (a_result, b_field_value) + b_result = Intersect(a_result[field_name], b_field_value) + return (a_result, b_result) + diff --git a/type_inference/research/infer.py b/type_inference/research/infer.py new file mode 100644 index 00000000..b6b3b543 --- /dev/null +++ b/type_inference/research/infer.py @@ -0,0 +1,716 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + +import hashlib +import json +import sys + +if '.' not in __package__: + from type_inference.research import reference_algebra + from type_inference.research import types_of_builtins + from common import color +else: + from ..research import reference_algebra + from ..research import types_of_builtins + try: + from ...common import color + except: + from common import color + + +class ContextualizedError: + def __init__(self): + self.type_error = None + self.context_string = None + self.refers_to_variable = None + self.refers_to_expression = None + + def Replace(self, type_error, context_string, refers_to_variable, refers_to_expression): + self.type_error = type_error + self.context_string = context_string + self.refers_to_variable = refers_to_variable + self.refers_to_expression = refers_to_expression + + def ReplaceIfMoreUseful(self, type_error, context_string, refers_to_variable, refers_to_expression): + if self.type_error is None: + self.Replace(type_error, context_string, refers_to_variable, refers_to_expression) + if self.context_string == 'UNKNOWN LOCATION': + self.Replace(type_error, context_string, refers_to_variable, refers_to_expression) + elif self.refers_to_variable is None and refers_to_variable: + self.Replace(type_error, context_string, refers_to_variable, refers_to_expression) + elif 'literal' in self.refers_to_expression: + self.Replace(type_error, context_string, refers_to_variable, refers_to_expression) + + else: + pass + + @classmethod + def BuildNiceMessage(self, context_string, error_message): + result_lines = [ + color.Format('{underline}Type analysis:{end}'), + context_string, '', + color.Format('[ {error}Error{end} ] ') + error_message] + return '\n'.join(result_lines) + + def NiceMessage(self): + if (isinstance(self.type_error[0], str) and + self.type_error[0].startswith('VERBATIM:')): + return self.type_error[0].removeprefix('VERBATIM:') + return self.BuildNiceMessage(self.context_string, + self.HelpfulErrorMessage()) + + def HelpfulErrorMessage(self): + result = str(self.type_error) + + if self.refers_to_variable: + result = color.Format( + 'Variable {warning}%s{end} ' % + self.refers_to_variable) + result + else: + result = color.Format( + 'Expression {warning}{e}{end} ', + args_dict=dict(e=str(self.refers_to_expression['expression_heritage']))) + result + return result + + +def ExpressionFields(): + return ['expression', 'left_hand_side', 'right_hand_side', + 'condition', 'consequence', 'otherwise'] + +def ExpressionsIterator(node): + for f in ExpressionFields(): + if f in node: + yield node[f] + + if 'record' in node and 'field_value' not in node['record']: + # if 'record' in node and 'expression_heritage' in node: + yield node['record'] + if 'the_list' in node: + for e in node['the_list']['element']: + yield e + if 'inclusion' in node: + yield node['inclusion']['element'] + yield node['inclusion']['list'] + + +def Walk(node, act): + """Walking over a dictionary of lists, acting on each element.""" + if isinstance(node, list): + for v in node: + Walk(v, act) + if isinstance(node, dict): + act(node) + for k in node: + if k != 'type': + Walk(node[k], act) + + +def ActMindingPodLiterals(node): + for e in ExpressionsIterator(node): + if 'literal' in e: + if 'the_number' in e['literal']: + reference_algebra.Unify(e['type']['the_type'], reference_algebra.TypeReference('Num')) + if 'the_string' in e['literal']: + reference_algebra.Unify(e['type']['the_type'], reference_algebra.TypeReference('Str')) + if 'the_bool' in e['literal']: + reference_algebra.Unify(e['type']['the_type'], reference_algebra.TypeReference('Bool')) + +def ActClearingTypes(node): + if 'type' in node: + del node['type'] + +def ActRememberingTypes(node): + if 'type' in node: + node['remembered_type'] = json.dumps(node['type']['the_type']) + +def ActRecallingTypes(node): + if 'remembered_type' in node: + remembered_type = reference_algebra.Revive( + json.loads(node['remembered_type'])) + reference_algebra.Unify( + node['type']['the_type'], + remembered_type + ) + +class TypesInferenceEngine: + def __init__(self, parsed_rules): + self.parsed_rules = parsed_rules + self.predicate_argumets_types = {} + self.dependencies = BuildDependencies(self.parsed_rules) + self.complexities = BuildComplexities(self.dependencies) + self.parsed_rules = list(sorted(self.parsed_rules, key=lambda x: self.complexities[x['head']['predicate_name']])) + self.predicate_signature = types_of_builtins.TypesOfBultins() + self.typing_preamble = None + self.collector = None + + def CollectTypes(self): + collector = TypeCollector(self.parsed_rules) + collector.CollectTypes() + self.typing_preamble = collector.typing_preamble + self.collector = collector + + def UpdateTypes(self, rule): + predicate_name = rule['head']['predicate_name'] + if predicate_name in self.predicate_signature: + predicate_signature = self.predicate_signature[predicate_name] + else: + predicate_signature = {} + for fv in rule['head']['record']['field_value']: + field_name = fv['field'] + predicate_signature[field_name] = reference_algebra.TypeReference('Any') + self.predicate_signature[predicate_name] = predicate_signature + + for fv in rule['head']['record']['field_value']: + field_name = fv['field'] + v = fv['value'] + if 'expression' in v: + value = v['expression'] + else: + value = v['aggregation']['expression'] + value_type = value['type']['the_type'] + if field_name not in predicate_signature: + raise TypeErrorCaughtException( + ContextualizedError.BuildNiceMessage( + rule['full_text'], + color.Format( + 'Predicate {warning}%s{end} has ' % predicate_name + + 'inconcistent rules, some include field ') + + color.Format('{warning}%s{end}' % field_name) + ' while others do not.')) + reference_algebra.Unify( + predicate_signature[field_name], + value_type) + + + def InferTypes(self): + for rule in self.parsed_rules: + if rule['head']['predicate_name'][0] == '@': + continue + t = TypeInferenceForRule(rule, self.predicate_signature) + t.PerformInference() + self.UpdateTypes(rule) + + for rule in self.parsed_rules: + Walk(rule, ConcretizeTypes) + self.CollectTypes() + + def ShowPredicateTypes(self): + result_lines = [] + for predicate_name, signature in self.predicate_signature.items(): + result_lines.append(RenderPredicateSignature(predicate_name, signature)) + return '\n'.join(result_lines) + + +def ConcretizeTypes(node): + if isinstance(node, dict): + if 'type' in node: + node['type']['the_type'] = reference_algebra.VeryConcreteType( + node['type']['the_type']) + + +def BuildDependencies(rules): + def ExtractDendencies(rule): + p = rule['head']['predicate_name'] + dependencies = [] + def ExtractPredicateName(node): + if 'predicate_name' in node: + dependencies.append(node['predicate_name']) + Walk(rule, ExtractPredicateName) + return p, dependencies + result = {} + for rule in rules: + p, ds = ExtractDendencies(rule) + result[p] = list(set(sorted(set(ds) - set([p]))) | set(result.get(p, []))) + return result + +def BuildComplexities(dependencies): + result = {} + def GetComplexity(p): + if p not in dependencies: + return 0 + if p not in result: + result[p] = 1 + result[p] = 1 + sum(GetComplexity(x) for x in dependencies[p]) + return result[p] + for p in dependencies: + GetComplexity(p) + return result + + +class TypeInferenceForRule: + def __init__(self, rule, types_of_builtins): + self.rule = rule + self.variable_type = {} + self.type_id_counter = 0 + self.found_error = None + self.types_of_builtins = types_of_builtins + + def PerformInference(self): + self.InitTypes() + self.MindPodLiterals() + self.MindBuiltinFieldTypes() + self.IterateInference() + + def GetTypeId(self): + result = self.type_id_counter + self.type_id_counter += 1 + return result + + def ActInitializingTypes(self, node): + for e in ExpressionsIterator(node): + if 'variable' not in e: # Variables are convered separately. + e['type'] = { + 'the_type': reference_algebra.TypeReference('Any'), + 'type_id': self.GetTypeId()} + + def InitTypes(self): + WalkInitializingVariables(self.rule, self.GetTypeId) + Walk(self.rule, self.ActInitializingTypes) + + def MindPodLiterals(self): + Walk(self.rule, ActMindingPodLiterals) + + def ActMindingBuiltinFieldTypes(self, node): + def InstillTypes(predicate_name, + field_value, signature, output_value): + copier = reference_algebra.TypeStructureCopier() + copy = copier.CopyConcreteOrReferenceType + if output_value: + output_value_type = output_value['type']['the_type'] + if 'logica_value' in signature: + reference_algebra.Unify( + output_value_type, + copy(signature['logica_value'])) + else: + error_message = ( + ContextualizedError.BuildNiceMessage( + output_value['expression_heritage'].Display(), + 'Predicate %s is not a function, but was called as such.' % + color.Format('{warning}%s{end}') % predicate_name, + ) + ) + error = reference_algebra.BadType( + ('VERBATIM:' + error_message, + output_value_type.target)) + output_value_type.target = reference_algebra.TypeReference.To(error) + + for fv in field_value: + field_name = fv['field'] + if (field_name not in signature and + isinstance(field_name, int) and + 'col%d' % field_name in signature): + field_name = 'col%d' % field_name + if field_name in signature: + reference_algebra.Unify( + fv['value']['expression']['type']['the_type'], + copy(signature[field_name])) + elif field_name == '*': + args = copy(reference_algebra.ClosedRecord(signature)) + reference_algebra.Unify( + fv['value']['expression']['type']['the_type'], + reference_algebra.TypeReference.To(args)) + elif '*' in signature: + args = copy(signature['*']) + reference_algebra.UnifyRecordField( + args, field_name, + fv['value']['expression']['type']['the_type']) + if isinstance(args.Target(), reference_algebra.BadType): + error_message = ( + ContextualizedError.BuildNiceMessage( + fv['value']['expression']['expression_heritage'].Display(), + 'Predicate %s does not have argument %s, but it was addressed.' % + (color.Format('{warning}%s{end}') % predicate_name, + color.Format('{warning}%s{end}') % fv['field']) + ) + ) + error = reference_algebra.BadType( + ('VERBATIM:' + error_message, + fv['value']['expression']['type']['the_type'].target)) + fv['value']['expression']['type']['the_type'].target = ( + reference_algebra.TypeReference.To(error)) + else: + error_message = ( + ContextualizedError.BuildNiceMessage( + fv['value']['expression']['expression_heritage'].Display(), + 'Predicate %s does not have argument %s, but it was addressed.' % + (color.Format('{warning}%s{end}') % predicate_name, + color.Format('{warning}%s{end}') % fv['field']) + ) + ) + error = reference_algebra.BadType( + ('VERBATIM:' + error_message, + fv['value']['expression']['type']['the_type'].target)) + fv['value']['expression']['type']['the_type'].target = ( + reference_algebra.TypeReference.To(error)) + + for e in ExpressionsIterator(node): + if 'call' in e: + p = e['call']['predicate_name'] + if p in self.types_of_builtins: + InstillTypes(p, e['call']['record']['field_value'], + self.types_of_builtins[p], e) + + if 'predicate' in node: + p = node['predicate']['predicate_name'] + if p in self.types_of_builtins: + InstillTypes(p, node['predicate']['record']['field_value'], + self.types_of_builtins[p], None) + + if 'head' in node: + p = node['head']['predicate_name'] + if p in self.types_of_builtins: + InstillTypes(p, node['head']['record']['field_value'], + self.types_of_builtins[p], None) + + + def MindBuiltinFieldTypes(self): + Walk(self.rule, self.ActMindingBuiltinFieldTypes) + + def ActUnifying(self, node): + if 'unification' in node: + left_type = node['unification']['left_hand_side']['type']['the_type'] + right_type = node['unification']['right_hand_side']['type']['the_type'] + reference_algebra.Unify(left_type, right_type) + + def ActUnderstandingSubscription(self, node): + if 'subscript' in node and 'record' in node['subscript']: + record_type = node['subscript']['record']['type']['the_type'] + field_type = node['type']['the_type'] + field_name = node['subscript']['subscript']['literal']['the_symbol']['symbol'] + reference_algebra.UnifyRecordField( + record_type, field_name, field_type) + + def ActMindingRecordLiterals(self, node): + if 'type' in node and 'record' in node: + record_type = node['type']['the_type'] + reference_algebra.Unify( + record_type, + reference_algebra.TypeReference( + reference_algebra.OpenRecord())) + for fv in node['record']['field_value']: + field_type = fv['value']['expression']['type']['the_type'] + field_name = fv['field'] + reference_algebra.UnifyRecordField( + record_type, field_name, field_type) + # print('>>>', node) + + node['type']['the_type'].CloseRecord() + + def ActMindingTypingPredicateLiterals(self, node): + if 'type' in node and 'literal' in node and 'the_predicate' in node['literal']: + predicate_name = node['literal']['the_predicate']['predicate_name'] + if predicate_name in ['Str', 'Num', 'Bool']: + reference_algebra.Unify(node['type']['the_type'], + reference_algebra.TypeReference(predicate_name)) + + def ActMindingListLiterals(self, node): + if 'type' in node and 'literal' in node and 'the_list' in node['literal']: + list_type = node['type']['the_type'] + for e in node['literal']['the_list']['element']: + reference_algebra.UnifyListElement( + list_type, e['type']['the_type']) + + def ActMindingInclusion(self, node): + if 'inclusion' in node: + list_type = node['inclusion']['list']['type']['the_type'] + element_type = node['inclusion']['element']['type']['the_type'] + reference_algebra.UnifyListElement( + list_type, element_type + ) + + def ActMindingCombine(self, node): + if 'combine' in node: + field_value = node['combine']['head']['record']['field_value'] + [logica_value] = [fv['value'] + for fv in field_value + if fv['field'] == 'logica_value'] + reference_algebra.Unify( + node['type']['the_type'], + logica_value['aggregation']['expression']['type']['the_type'] + ) + + def ActMindingImplications(self, node): + if 'implication' in node: + for if_then in node['implication']['if_then']: + reference_algebra.Unify( + node['type']['the_type'], + if_then['consequence']['type']['the_type'] + ) + reference_algebra.Unify( + node['type']['the_type'], + node['implication']['otherwise']['type']['the_type'] + ) + + def IterateInference(self): + Walk(self.rule, self.ActMindingTypingPredicateLiterals) + Walk(self.rule, self.ActMindingRecordLiterals) + Walk(self.rule, self.ActUnifying) + Walk(self.rule, self.ActUnderstandingSubscription) + Walk(self.rule, self.ActMindingListLiterals) + Walk(self.rule, self.ActMindingInclusion) + Walk(self.rule, self.ActMindingCombine) + Walk(self.rule, self.ActMindingImplications) + +def RenderPredicateSignature(predicate_name, signature): + def FieldValue(f, v): + if isinstance(f, int): + field = '' + else: + field = f + ': ' + value = reference_algebra.RenderType(reference_algebra.VeryConcreteType(v)) + return field + value + field_values = [FieldValue(f, v) for f, v in signature.items() + if f != 'logica_value'] + signature_str = ', '.join(field_values) + maybe_value = [ + ' = ' + reference_algebra.RenderType( + reference_algebra.VeryConcreteType(v)) + for f, v in signature.items() if f == 'logica_value'] + value_or_nothing = maybe_value[0] if maybe_value else '' + result = f'type {predicate_name}({signature_str}){value_or_nothing};' + return result + + +class TypeInferenceForStructure: + def __init__(self, structure, signatures): + self.structure = structure + self.signatures = signatures + self.collector = None + + def PerformInference(self): + quazy_rule = self.BuildQuazyRule() + Walk(quazy_rule, ActRememberingTypes) + Walk(quazy_rule, ActClearingTypes) + inferencer = TypeInferenceForRule(quazy_rule, self.signatures) + inferencer.PerformInference() + Walk(quazy_rule, ActRecallingTypes) + + Walk(quazy_rule, ConcretizeTypes) + collector = TypeCollector([quazy_rule]) + collector.CollectTypes() + self.collector = collector + + import json + # + # print('>> quazy rule:', json.dumps(quazy_rule, indent=' ')) + + def BuildQuazyRule(self): + result = {} + result['quazy_body'] = self.BuildQuazyBody() + result['select'] = self.BuildSelect() + result['unnestings'] = self.BuildUnnestings() + result['constraints'] = self.structure.constraints + return result + + def BuildUnnestings(self): + # print('>> unnestings', self.structure.unnestings) + result = [] + for variable, the_list in self.structure.unnestings: + result.append( + {'inclusion': { + 'element': variable, + 'list': the_list + }} + ) + return result + + def BuildQuazyBody(self): + # print('>> tables, vars_map: ', self.structure.tables, self.structure.vars_map) + calls = {} + + for table_id, predicate in self.structure.tables.items(): + calls[table_id] = { + 'predicate': {'predicate_name': predicate, + 'record': {'field_value': []}}} + + for (table_id, field), variable in self.structure.vars_map.items(): + if table_id is None: + # This is from unnestings. I don't recall why are those even here :-( + # Need to clarify. + continue + calls[table_id]['predicate']['record']['field_value'].append( + {'field': field, + 'value': {'expression': {'variable': {'var_name': variable}}} + } + ) + + return list(calls.values()) + + def BuildSelect(self): + field_values = [] + result = {'record': {'field_value': field_values}} + for k, v in self.structure.select.items(): + field_values.append({ + 'field': k, + 'value': {'expression': v} + }) + return result + +class TypeErrorCaughtException(Exception): + """Exception thrown when user-error is detected at rule-compile time.""" + + def __init__(self, message): + super(TypeErrorCaughtException, self).__init__(message) + + def ShowMessage(self, stream=sys.stderr): + print(str(self), file=stream) + + +class TypeErrorChecker: + def __init__(self, typed_rules): + self.typed_rules = typed_rules + + def CheckForError(self, mode='print'): + self.found_error = self.SearchTypeErrors() + if self.found_error.type_error: + if mode == 'print': + print(self.found_error.NiceMessage()) + elif mode == 'raise': + raise TypeErrorCaughtException(self.found_error.NiceMessage()) + else: + assert False + + def SearchTypeErrors(self): + found_error = ContextualizedError() + def LookForError(node): + nonlocal found_error + if 'type' in node: + t = reference_algebra.VeryConcreteType(node['type']['the_type']) + if isinstance(t, reference_algebra.BadType): + if 'variable' in node: + v = node['variable']['var_name'] + else: + v = None + # Combines don't have expression_heritage. + # assert 'expression_heritage' in node, (node, t) + if 'expression_heritage' not in node: + found_error.ReplaceIfMoreUseful( + t, 'UNKNOWN LOCATION', v, + node) + else: + found_error.ReplaceIfMoreUseful( + t, node['expression_heritage'].Display(), v, + node) + for rule in self.typed_rules: + Walk(rule, LookForError) + if found_error.type_error: + return found_error + return found_error + + +def WalkInitializingVariables(node, get_type): + """Initialize variables minding combines contexts.""" + type_of_variable = {} + def Jog(node, found_combines): + nonlocal type_of_variable + if isinstance(node, list): + for v in node: + Jog(v, found_combines) + if isinstance(node, dict): + if 'variable' in node: + var_name = node['variable']['var_name'] + if var_name not in type_of_variable: + type_of_variable[var_name] = { + 'the_type': reference_algebra.TypeReference('Any'), + 'type_id': get_type()} + node['type'] = type_of_variable[var_name] + for k in node: + if k != 'type': + if k != 'combine': + Jog(node[k], found_combines) + else: + found_combines.append(node[k]) + def JogPredicate(node): + nonlocal type_of_variable + found_combines = [] + Jog(node, found_combines) + backed_up_types = {k: v for k, v in type_of_variable.items()} + for n in found_combines: + JogPredicate(n) + type_of_variable = backed_up_types + JogPredicate(node) + +def Fingerprint(s): + return int(hashlib.md5(str(s).encode()).hexdigest()[:16], 16) + +def RecordTypeName(type_render): + return 'logicarecord%d' % (Fingerprint(type_render) % 1000000000) + +class TypeCollector: + def __init__(self, parsed_rules): + self.parsed_rules = parsed_rules + self.type_map = {} + self.psql_struct_type_name = {} + self.psql_type_definition = {} + self.definitions = [] + self.typing_preamble = '' + + def ActPopulatingTypeMap(self, node): + if 'type' in node: + t = node['type']['the_type'] + t_rendering = reference_algebra.RenderType(t) + self.type_map[t_rendering] = t + node['type']['rendered_type'] = t_rendering + if isinstance(t, dict) and reference_algebra.IsFullyDefined(t): + node['type']['type_name'] = RecordTypeName(t_rendering) + if isinstance(t, list) and reference_algebra.IsFullyDefined(t): + [e] = t + node['type']['element_type_name'] = self.PsqlType(e) + + def CollectTypes(self): + Walk(self.parsed_rules, self.ActPopulatingTypeMap) + for t in self.type_map: + the_type = self.type_map[t] + if isinstance(the_type, dict): + if not reference_algebra.IsFullyDefined(the_type): + continue + self.psql_struct_type_name[t] = RecordTypeName(t) + self.BuildPsqlDefinitions() + + def PsqlType(self, t): + if t == 'Str': + return 'text' + if t == 'Num': + return 'numeric' + if t == 'Bool': + return 'bool' + if isinstance(t, dict): + return RecordTypeName(reference_algebra.RenderType(t)) + if isinstance(t, list): + [e] = t + return self.PsqlType(e) + '[]' + assert False, t + + def BuildPsqlDefinitions(self): + for t in self.psql_struct_type_name: + arg_name = lambda x: x if isinstance(x, str) else 'col%d' % x + args = ', '.join( + arg_name(f) + ' ' + self.PsqlType(v) + for f, v in sorted(self.type_map[t].items()) + ) + self.psql_type_definition[t] = f'create type %s as (%s);' % ( + self.psql_struct_type_name[t], args) + + wrap = lambda n, d: ( + f"-- Logica type: {n}\n" + + f"if not exists (select 'I(am) :- I(think)' from pg_type where typname = '{n}') then {d} end if;" + ) + self.definitions = { + t: wrap(self.psql_struct_type_name[t], self.psql_type_definition[t]) + for t in sorted(self.psql_struct_type_name, key=len)} + self.typing_preamble = BuildPreamble(self.definitions) + +def BuildPreamble(definitions): + return 'DO $$\nBEGIN\n' + '\n'.join(definitions.values()) + '\nEND $$;\n' diff --git a/type_inference/research/integration_tests/run_tests.py b/type_inference/research/integration_tests/run_tests.py new file mode 100644 index 00000000..81a04057 --- /dev/null +++ b/type_inference/research/integration_tests/run_tests.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# +# Copyright 2020 Google LLC +# +# 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. + +"""A suite of tests for type inference.""" + +from common import logica_test + +def RunTypesTest(name, src=None, golden=None): + src = src or ( + 'type_inference/research/integration_tests/' + name + '.l') + golden = golden or ( + 'type_inference/research/integration_tests/' + name + '.txt') + logica_test.TestManager.RunTypesTest(name, src, golden) + +def RunAll(): + RunTypesTest('typing_palindrome_puzzle_test') + RunTypesTest('typing_kitchensync_test') + RunTypesTest('typing_basic_test') + RunTypesTest('typing_aggregation_test') + RunTypesTest('typing_lists_test') + RunTypesTest('typing_nested_test') + RunTypesTest('typing_combines_test') + RunTypesTest('typing_combine2_test') + +if __name__ == '__main__': + RunAll() \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_aggregation_test.l b/type_inference/research/integration_tests/typing_aggregation_test.l new file mode 100644 index 00000000..10bd257c --- /dev/null +++ b/type_inference/research/integration_tests/typing_aggregation_test.l @@ -0,0 +1,17 @@ +#!/usr/bin/python +# +# Copyright 2020 Google LLC +# +# 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. + +Test(a:, b? += t) distinct :- T(a, t), Str(a); \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_aggregation_test.txt b/type_inference/research/integration_tests/typing_aggregation_test.txt new file mode 100644 index 00000000..62550ff1 --- /dev/null +++ b/type_inference/research/integration_tests/typing_aggregation_test.txt @@ -0,0 +1,138 @@ +[ + { + "body": { + "conjunction": { + "conjunct": [ + { + "predicate": { + "predicate_name": "T", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "a", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + }, + "variable": { + "var_name": "a" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "t", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "t" + } + } + } + } + ] + } + } + }, + { + "predicate": { + "predicate_name": "Str", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "a", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + }, + "variable": { + "var_name": "a" + } + } + } + } + ] + } + } + } + ] + } + }, + "distinct_denoted": true, + "full_text": "Test(a:, b? += t) distinct :- T(a, t), Str(a)", + "head": { + "predicate_name": "Test", + "record": { + "field_value": [ + { + "field": "a", + "value": { + "expression": { + "expression_heritage": "a", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + }, + "variable": { + "var_name": "a" + } + } + } + }, + { + "field": "b", + "value": { + "aggregation": { + "expression": { + "call": { + "predicate_name": "Agg+", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "t", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "t" + } + } + } + } + ] + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 2 + } + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_basic_test.l b/type_inference/research/integration_tests/typing_basic_test.l new file mode 100644 index 00000000..e23f08af --- /dev/null +++ b/type_inference/research/integration_tests/typing_basic_test.l @@ -0,0 +1,18 @@ +#!/usr/bin/python +# +# Copyright 2020 Google LLC +# +# 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. + +Q(1, "a", x, z, w.a, {t: 1, z: "a", r: {z: 1}}) :- + x == 1 + z, T(z, w, l), e in l, w.a.b == 7; \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_basic_test.txt b/type_inference/research/integration_tests/typing_basic_test.txt new file mode 100644 index 00000000..ea8af478 --- /dev/null +++ b/type_inference/research/integration_tests/typing_basic_test.txt @@ -0,0 +1,451 @@ +[ + { + "body": { + "conjunction": { + "conjunct": [ + { + "unification": { + "left_hand_side": { + "expression_heritage": "x", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "x" + } + }, + "right_hand_side": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 14 + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "z", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "z" + } + } + } + } + ] + } + }, + "expression_heritage": "1 + z", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 13 + } + } + } + }, + { + "predicate": { + "predicate_name": "T", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "z", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "z" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "w", + "type": { + "rendered_type": "{a: {b: Num}}", + "the_type": { + "a": { + "b": "Num" + } + }, + "type_id": 2, + "type_name": "logicarecord715995786" + }, + "variable": { + "var_name": "w" + } + } + } + }, + { + "field": 2, + "value": { + "expression": { + "expression_heritage": "l", + "type": { + "rendered_type": "[Singular]", + "the_type": [ + "Singular" + ], + "type_id": 3 + }, + "variable": { + "var_name": "l" + } + } + } + } + ] + } + } + }, + { + "inclusion": { + "element": { + "expression_heritage": "e", + "type": { + "rendered_type": "Singular", + "the_type": "Singular", + "type_id": 4 + }, + "variable": { + "var_name": "e" + } + }, + "list": { + "expression_heritage": "l", + "type": { + "rendered_type": "[Singular]", + "the_type": [ + "Singular" + ], + "type_id": 3 + }, + "variable": { + "var_name": "l" + } + } + } + }, + { + "unification": { + "left_hand_side": { + "expression_heritage": "w.a.b", + "subscript": { + "record": { + "expression_heritage": "w.a", + "subscript": { + "record": { + "expression_heritage": "w", + "type": { + "rendered_type": "{a: {b: Num}}", + "the_type": { + "a": { + "b": "Num" + } + }, + "type_id": 2, + "type_name": "logicarecord715995786" + }, + "variable": { + "var_name": "w" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "a" + } + } + } + }, + "type": { + "rendered_type": "{b: Num}", + "the_type": { + "b": "Num" + }, + "type_id": 17, + "type_name": "logicarecord958681958" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "b" + } + } + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 15 + } + }, + "right_hand_side": { + "expression_heritage": "7", + "literal": { + "the_number": { + "number": "7" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 16 + } + } + } + } + ] + } + }, + "full_text": "Q(1, \"a\", x, z, w.a, {t: 1, z: \"a\", r: {z: 1}}) :-\n x == 1 + z, T(z, w, l), e in l, w.a.b == 7", + "head": { + "predicate_name": "Q", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "\"a\"", + "literal": { + "the_string": { + "the_string": "a" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 6 + } + } + } + }, + { + "field": 2, + "value": { + "expression": { + "expression_heritage": "x", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "x" + } + } + } + }, + { + "field": 3, + "value": { + "expression": { + "expression_heritage": "z", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "z" + } + } + } + }, + { + "field": 4, + "value": { + "expression": { + "expression_heritage": "w.a", + "subscript": { + "record": { + "expression_heritage": "w", + "type": { + "rendered_type": "{a: {b: Num}}", + "the_type": { + "a": { + "b": "Num" + } + }, + "type_id": 2, + "type_name": "logicarecord715995786" + }, + "variable": { + "var_name": "w" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "a" + } + } + } + }, + "type": { + "rendered_type": "{b: Num}", + "the_type": { + "b": "Num" + }, + "type_id": 7, + "type_name": "logicarecord958681958" + } + } + } + }, + { + "field": 5, + "value": { + "expression": { + "expression_heritage": "{t: 1, z: \"a\", r: {z: 1}}", + "record": { + "field_value": [ + { + "field": "t", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 9 + } + } + } + }, + { + "field": "z", + "value": { + "expression": { + "expression_heritage": "\"a\"", + "literal": { + "the_string": { + "the_string": "a" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 10 + } + } + } + }, + { + "field": "r", + "value": { + "expression": { + "expression_heritage": "{z: 1}", + "record": { + "field_value": [ + { + "field": "z", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 12 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{z: Num}", + "the_type": { + "z": "Num" + }, + "type_id": 11, + "type_name": "logicarecord574638620" + } + } + } + } + ] + }, + "type": { + "rendered_type": "{r: {z: Num}, t: Num, z: Str}", + "the_type": { + "r": { + "z": "Num" + }, + "t": "Num", + "z": "Str" + }, + "type_id": 8, + "type_name": "logicarecord412758286" + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_combine2_test.l b/type_inference/research/integration_tests/typing_combine2_test.l new file mode 100644 index 00000000..ba80c451 --- /dev/null +++ b/type_inference/research/integration_tests/typing_combine2_test.l @@ -0,0 +1,21 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +T(1); + +Test(x,{o:1}) :- x List= ({a:} :- a in [{z:}]), T(z); \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_combine2_test.txt b/type_inference/research/integration_tests/typing_combine2_test.txt new file mode 100644 index 00000000..886d8f8e --- /dev/null +++ b/type_inference/research/integration_tests/typing_combine2_test.txt @@ -0,0 +1,345 @@ +[ + { + "full_text": "@Engine(\"psql\")", + "head": { + "predicate_name": "@Engine", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "\"psql\"", + "literal": { + "the_string": { + "the_string": "psql" + } + } + } + } + } + ] + } + } + }, + { + "full_text": "T(1)", + "head": { + "predicate_name": "T", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + } + } + } + } + ] + } + } + }, + { + "body": { + "conjunction": { + "conjunct": [ + { + "unification": { + "left_hand_side": { + "expression_heritage": "x", + "type": { + "element_type_name": "logicarecord350574256", + "rendered_type": "[{a: {z: Num}}]", + "the_type": [ + { + "a": { + "z": "Num" + } + } + ], + "type_id": 0 + }, + "variable": { + "var_name": "x" + } + }, + "right_hand_side": { + "combine": { + "body": { + "conjunction": { + "conjunct": [ + { + "inclusion": { + "element": { + "expression_heritage": "a", + "type": { + "rendered_type": "{z: Num}", + "the_type": { + "z": "Num" + }, + "type_id": 2, + "type_name": "logicarecord574638620" + }, + "variable": { + "var_name": "a" + } + }, + "list": { + "expression_heritage": "[{z:}]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "{z:}", + "record": { + "field_value": [ + { + "field": "z", + "value": { + "expression": { + "expression_heritage": "z", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "z" + } + } + } + } + ] + }, + "type": { + "rendered_type": "{z: Num}", + "the_type": { + "z": "Num" + }, + "type_id": 9, + "type_name": "logicarecord574638620" + } + } + ] + } + }, + "type": { + "element_type_name": "logicarecord574638620", + "rendered_type": "[{z: Num}]", + "the_type": [ + { + "z": "Num" + } + ], + "type_id": 8 + } + } + } + } + ] + } + }, + "distinct_denoted": true, + "full_text": "x List= ({a:} :- a in [{z:}])", + "head": { + "predicate_name": "Combine", + "record": { + "field_value": [ + { + "field": "logica_value", + "value": { + "aggregation": { + "expression": { + "call": { + "predicate_name": "List", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "{a:}", + "record": { + "field_value": [ + { + "field": "a", + "value": { + "expression": { + "expression_heritage": "a", + "type": { + "rendered_type": "{z: Num}", + "the_type": { + "z": "Num" + }, + "type_id": 2, + "type_name": "logicarecord574638620" + }, + "variable": { + "var_name": "a" + } + } + } + } + ] + }, + "type": { + "rendered_type": "{a: {z: Num}}", + "the_type": { + "a": { + "z": "Num" + } + }, + "type_id": 7, + "type_name": "logicarecord350574256" + } + } + } + } + ] + } + }, + "type": { + "element_type_name": "logicarecord350574256", + "rendered_type": "[{a: {z: Num}}]", + "the_type": [ + { + "a": { + "z": "Num" + } + } + ], + "type_id": 6 + } + } + } + } + } + ] + } + } + }, + "type": { + "element_type_name": "logicarecord350574256", + "rendered_type": "[{a: {z: Num}}]", + "the_type": [ + { + "a": { + "z": "Num" + } + } + ], + "type_id": 5 + } + } + } + }, + { + "predicate": { + "predicate_name": "T", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "z", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "z" + } + } + } + } + ] + } + } + } + ] + } + }, + "full_text": "Test(x,{o:1}) :- x List= ({a:} :- a in [{z:}]), T(z)", + "head": { + "predicate_name": "Test", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "x", + "type": { + "element_type_name": "logicarecord350574256", + "rendered_type": "[{a: {z: Num}}]", + "the_type": [ + { + "a": { + "z": "Num" + } + } + ], + "type_id": 0 + }, + "variable": { + "var_name": "x" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "{o:1}", + "record": { + "field_value": [ + { + "field": "o", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 4 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{o: Num}", + "the_type": { + "o": "Num" + }, + "type_id": 3, + "type_name": "logicarecord944799139" + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_combines_test.l b/type_inference/research/integration_tests/typing_combines_test.l new file mode 100644 index 00000000..67dbb6a2 --- /dev/null +++ b/type_inference/research/integration_tests/typing_combines_test.l @@ -0,0 +1,19 @@ +#!/usr/bin/python +# +# Copyright 2020 Google LLC +# +# 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. + +Test(x, y) :- + x += (a :- a in [1,2,3]), + y List= (a :- a in ["a", "b"]) diff --git a/type_inference/research/integration_tests/typing_combines_test.txt b/type_inference/research/integration_tests/typing_combines_test.txt new file mode 100644 index 00000000..8bf1fe86 --- /dev/null +++ b/type_inference/research/integration_tests/typing_combines_test.txt @@ -0,0 +1,343 @@ +[ + { + "body": { + "conjunction": { + "conjunct": [ + { + "unification": { + "left_hand_side": { + "expression_heritage": "x", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "x" + } + }, + "right_hand_side": { + "combine": { + "body": { + "conjunction": { + "conjunct": [ + { + "inclusion": { + "element": { + "expression_heritage": "a", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 2 + }, + "variable": { + "var_name": "a" + } + }, + "list": { + "expression_heritage": "[1,2,3]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 7 + } + }, + { + "expression_heritage": "2", + "literal": { + "the_number": { + "number": "2" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 8 + } + }, + { + "expression_heritage": "3", + "literal": { + "the_number": { + "number": "3" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 9 + } + } + ] + } + }, + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 6 + } + } + } + } + ] + } + }, + "distinct_denoted": true, + "full_text": "x += (a :- a in [1,2,3])", + "head": { + "predicate_name": "Combine", + "record": { + "field_value": [ + { + "field": "logica_value", + "value": { + "aggregation": { + "expression": { + "call": { + "predicate_name": "Agg+", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "a", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 2 + }, + "variable": { + "var_name": "a" + } + } + } + } + ] + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + } + } + } + } + } + ] + } + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 4 + } + } + } + }, + { + "unification": { + "left_hand_side": { + "expression_heritage": "y", + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 1 + }, + "variable": { + "var_name": "y" + } + }, + "right_hand_side": { + "combine": { + "body": { + "conjunction": { + "conjunct": [ + { + "inclusion": { + "element": { + "expression_heritage": "a", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 3 + }, + "variable": { + "var_name": "a" + } + }, + "list": { + "expression_heritage": "[\"a\", \"b\"]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "\"a\"", + "literal": { + "the_string": { + "the_string": "a" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 13 + } + }, + { + "expression_heritage": "\"b\"", + "literal": { + "the_string": { + "the_string": "b" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 14 + } + } + ] + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 12 + } + } + } + } + ] + } + }, + "distinct_denoted": true, + "full_text": "y List= (a :- a in [\"a\", \"b\"])", + "head": { + "predicate_name": "Combine", + "record": { + "field_value": [ + { + "field": "logica_value", + "value": { + "aggregation": { + "expression": { + "call": { + "predicate_name": "List", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "a", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 3 + }, + "variable": { + "var_name": "a" + } + } + } + } + ] + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 11 + } + } + } + } + } + ] + } + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 10 + } + } + } + } + ] + } + }, + "full_text": "Test(x, y) :-\n x += (a :- a in [1,2,3]),\n y List= (a :- a in [\"a\", \"b\"])", + "head": { + "predicate_name": "Test", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "x", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "x" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "y", + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 1 + }, + "variable": { + "var_name": "y" + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_kitchensync_test.l b/type_inference/research/integration_tests/typing_kitchensync_test.l new file mode 100644 index 00000000..5109f44a --- /dev/null +++ b/type_inference/research/integration_tests/typing_kitchensync_test.l @@ -0,0 +1,42 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +Kitchensync( + name: "sync 1", + items: [{item: "plate", num: 10}, {item: "cup", num: 6}]); +Kitchensync( + name: "sync 2", + items: [{item: "plate", num: 15}, {item: "spoon", num: 20}]); +Kitchensync( + name: "luxury sync", + items: [{item: "fork", num: 5}, {item: "cup", num: 4}]); + +Test(name:, overview:) :- + Kitchensync(name:, items:), + Kitchensync(name:, items:), + overview List= ( + {item:, quantity:} :- + quantity = ( + if num > 9 then + "large" + else + "small" + ), + num > 5, + {item:, num:} in items + ); \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_kitchensync_test.txt b/type_inference/research/integration_tests/typing_kitchensync_test.txt new file mode 100644 index 00000000..17c9482d --- /dev/null +++ b/type_inference/research/integration_tests/typing_kitchensync_test.txt @@ -0,0 +1,1006 @@ +[ + { + "full_text": "@Engine(\"psql\")", + "head": { + "predicate_name": "@Engine", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "\"psql\"", + "literal": { + "the_string": { + "the_string": "psql" + } + } + } + } + } + ] + } + } + }, + { + "full_text": "Kitchensync(\n name: \"sync 1\", \n items: [{item: \"plate\", num: 10}, {item: \"cup\", num: 6}])", + "head": { + "predicate_name": "Kitchensync", + "record": { + "field_value": [ + { + "field": "name", + "value": { + "expression": { + "expression_heritage": "\"sync 1\"", + "literal": { + "the_string": { + "the_string": "sync 1" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + } + } + } + }, + { + "field": "items", + "value": { + "expression": { + "expression_heritage": "[{item: \"plate\", num: 10}, {item: \"cup\", num: 6}]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "{item: \"plate\", num: 10}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "\"plate\"", + "literal": { + "the_string": { + "the_string": "plate" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 4 + } + } + } + }, + { + "field": "num", + "value": { + "expression": { + "expression_heritage": "10", + "literal": { + "the_number": { + "number": "10" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, num: Num}", + "the_type": { + "item": "Str", + "num": "Num" + }, + "type_id": 2, + "type_name": "logicarecord454966611" + } + }, + { + "expression_heritage": "{item: \"cup\", num: 6}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "\"cup\"", + "literal": { + "the_string": { + "the_string": "cup" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 6 + } + } + } + }, + { + "field": "num", + "value": { + "expression": { + "expression_heritage": "6", + "literal": { + "the_number": { + "number": "6" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 7 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, num: Num}", + "the_type": { + "item": "Str", + "num": "Num" + }, + "type_id": 3, + "type_name": "logicarecord454966611" + } + } + ] + } + }, + "type": { + "element_type_name": "logicarecord454966611", + "rendered_type": "[{item: Str, num: Num}]", + "the_type": [ + { + "item": "Str", + "num": "Num" + } + ], + "type_id": 1 + } + } + } + } + ] + } + } + }, + { + "full_text": "Kitchensync(\n name: \"sync 2\", \n items: [{item: \"plate\", num: 15}, {item: \"spoon\", num: 20}])", + "head": { + "predicate_name": "Kitchensync", + "record": { + "field_value": [ + { + "field": "name", + "value": { + "expression": { + "expression_heritage": "\"sync 2\"", + "literal": { + "the_string": { + "the_string": "sync 2" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + } + } + } + }, + { + "field": "items", + "value": { + "expression": { + "expression_heritage": "[{item: \"plate\", num: 15}, {item: \"spoon\", num: 20}]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "{item: \"plate\", num: 15}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "\"plate\"", + "literal": { + "the_string": { + "the_string": "plate" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 4 + } + } + } + }, + { + "field": "num", + "value": { + "expression": { + "expression_heritage": "15", + "literal": { + "the_number": { + "number": "15" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, num: Num}", + "the_type": { + "item": "Str", + "num": "Num" + }, + "type_id": 2, + "type_name": "logicarecord454966611" + } + }, + { + "expression_heritage": "{item: \"spoon\", num: 20}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "\"spoon\"", + "literal": { + "the_string": { + "the_string": "spoon" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 6 + } + } + } + }, + { + "field": "num", + "value": { + "expression": { + "expression_heritage": "20", + "literal": { + "the_number": { + "number": "20" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 7 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, num: Num}", + "the_type": { + "item": "Str", + "num": "Num" + }, + "type_id": 3, + "type_name": "logicarecord454966611" + } + } + ] + } + }, + "type": { + "element_type_name": "logicarecord454966611", + "rendered_type": "[{item: Str, num: Num}]", + "the_type": [ + { + "item": "Str", + "num": "Num" + } + ], + "type_id": 1 + } + } + } + } + ] + } + } + }, + { + "full_text": "Kitchensync(\n name: \"luxury sync\", \n items: [{item: \"fork\", num: 5}, {item: \"cup\", num: 4}])", + "head": { + "predicate_name": "Kitchensync", + "record": { + "field_value": [ + { + "field": "name", + "value": { + "expression": { + "expression_heritage": "\"luxury sync\"", + "literal": { + "the_string": { + "the_string": "luxury sync" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + } + } + } + }, + { + "field": "items", + "value": { + "expression": { + "expression_heritage": "[{item: \"fork\", num: 5}, {item: \"cup\", num: 4}]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "{item: \"fork\", num: 5}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "\"fork\"", + "literal": { + "the_string": { + "the_string": "fork" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 4 + } + } + } + }, + { + "field": "num", + "value": { + "expression": { + "expression_heritage": "5", + "literal": { + "the_number": { + "number": "5" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, num: Num}", + "the_type": { + "item": "Str", + "num": "Num" + }, + "type_id": 2, + "type_name": "logicarecord454966611" + } + }, + { + "expression_heritage": "{item: \"cup\", num: 4}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "\"cup\"", + "literal": { + "the_string": { + "the_string": "cup" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 6 + } + } + } + }, + { + "field": "num", + "value": { + "expression": { + "expression_heritage": "4", + "literal": { + "the_number": { + "number": "4" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 7 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, num: Num}", + "the_type": { + "item": "Str", + "num": "Num" + }, + "type_id": 3, + "type_name": "logicarecord454966611" + } + } + ] + } + }, + "type": { + "element_type_name": "logicarecord454966611", + "rendered_type": "[{item: Str, num: Num}]", + "the_type": [ + { + "item": "Str", + "num": "Num" + } + ], + "type_id": 1 + } + } + } + } + ] + } + } + }, + { + "body": { + "conjunction": { + "conjunct": [ + { + "predicate": { + "predicate_name": "Kitchensync", + "record": { + "field_value": [ + { + "field": "name", + "value": { + "expression": { + "expression_heritage": "name", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + }, + "variable": { + "var_name": "name" + } + } + } + }, + { + "field": "items", + "value": { + "expression": { + "expression_heritage": "items", + "type": { + "element_type_name": "logicarecord454966611", + "rendered_type": "[{item: Str, num: Num}]", + "the_type": [ + { + "item": "Str", + "num": "Num" + } + ], + "type_id": 2 + }, + "variable": { + "var_name": "items" + } + } + } + } + ] + } + } + }, + { + "predicate": { + "predicate_name": "Kitchensync", + "record": { + "field_value": [ + { + "field": "name", + "value": { + "expression": { + "expression_heritage": "name", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + }, + "variable": { + "var_name": "name" + } + } + } + }, + { + "field": "items", + "value": { + "expression": { + "expression_heritage": "items", + "type": { + "element_type_name": "logicarecord454966611", + "rendered_type": "[{item: Str, num: Num}]", + "the_type": [ + { + "item": "Str", + "num": "Num" + } + ], + "type_id": 2 + }, + "variable": { + "var_name": "items" + } + } + } + } + ] + } + } + }, + { + "unification": { + "left_hand_side": { + "expression_heritage": "overview", + "type": { + "element_type_name": "logicarecord980116590", + "rendered_type": "[{item: Str, quantity: Str}]", + "the_type": [ + { + "item": "Str", + "quantity": "Str" + } + ], + "type_id": 1 + }, + "variable": { + "var_name": "overview" + } + }, + "right_hand_side": { + "combine": { + "body": { + "conjunction": { + "conjunct": [ + { + "predicate": { + "predicate_name": "=", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "quantity", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 4 + }, + "variable": { + "var_name": "quantity" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "if num > 9 then\n \"large\"\n else\n \"small\"", + "implication": { + "if_then": [ + { + "condition": { + "call": { + "predicate_name": ">", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "num", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + }, + "variable": { + "var_name": "num" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "9", + "literal": { + "the_number": { + "number": "9" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 13 + } + } + } + } + ] + } + }, + "expression_heritage": "num > 9", + "type": { + "rendered_type": "Bool", + "the_type": "Bool", + "type_id": 11 + } + }, + "consequence": { + "expression_heritage": "\"large\"", + "literal": { + "the_string": { + "the_string": "large" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 12 + } + } + } + ], + "otherwise": { + "expression_heritage": "\"small\"", + "literal": { + "the_string": { + "the_string": "small" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 10 + } + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 9 + } + } + } + } + ] + } + } + }, + { + "predicate": { + "predicate_name": ">", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "num", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + }, + "variable": { + "var_name": "num" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "5", + "literal": { + "the_number": { + "number": "5" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 14 + } + } + } + } + ] + } + } + }, + { + "inclusion": { + "element": { + "expression_heritage": "{item:, num:}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "item", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 3 + }, + "variable": { + "var_name": "item" + } + } + } + }, + { + "field": "num", + "value": { + "expression": { + "expression_heritage": "num", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + }, + "variable": { + "var_name": "num" + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, num: Num}", + "the_type": { + "item": "Str", + "num": "Num" + }, + "type_id": 15, + "type_name": "logicarecord454966611" + } + }, + "list": { + "expression_heritage": "items", + "type": { + "element_type_name": "logicarecord454966611", + "rendered_type": "[{item: Str, num: Num}]", + "the_type": [ + { + "item": "Str", + "num": "Num" + } + ], + "type_id": 2 + }, + "variable": { + "var_name": "items" + } + } + } + } + ] + } + }, + "distinct_denoted": true, + "full_text": "overview List= (\n {item:, quantity:} :-\n quantity = (\n if num > 9 then\n \"large\"\n else\n \"small\"\n ),\n num > 5,\n {item:, num:} in items\n )", + "head": { + "predicate_name": "Combine", + "record": { + "field_value": [ + { + "field": "logica_value", + "value": { + "aggregation": { + "expression": { + "call": { + "predicate_name": "List", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "{item:, quantity:}", + "record": { + "field_value": [ + { + "field": "item", + "value": { + "expression": { + "expression_heritage": "item", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 3 + }, + "variable": { + "var_name": "item" + } + } + } + }, + { + "field": "quantity", + "value": { + "expression": { + "expression_heritage": "quantity", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 4 + }, + "variable": { + "var_name": "quantity" + } + } + } + } + ] + }, + "type": { + "rendered_type": "{item: Str, quantity: Str}", + "the_type": { + "item": "Str", + "quantity": "Str" + }, + "type_id": 8, + "type_name": "logicarecord980116590" + } + } + } + } + ] + } + }, + "type": { + "element_type_name": "logicarecord980116590", + "rendered_type": "[{item: Str, quantity: Str}]", + "the_type": [ + { + "item": "Str", + "quantity": "Str" + } + ], + "type_id": 7 + } + } + } + } + } + ] + } + } + }, + "type": { + "element_type_name": "logicarecord980116590", + "rendered_type": "[{item: Str, quantity: Str}]", + "the_type": [ + { + "item": "Str", + "quantity": "Str" + } + ], + "type_id": 6 + } + } + } + } + ] + } + }, + "full_text": "Test(name:, overview:) :-\n Kitchensync(name:, items:),\n Kitchensync(name:, items:),\n overview List= (\n {item:, quantity:} :-\n quantity = (\n if num > 9 then\n \"large\"\n else\n \"small\"\n ),\n num > 5,\n {item:, num:} in items\n )", + "head": { + "predicate_name": "Test", + "record": { + "field_value": [ + { + "field": "name", + "value": { + "expression": { + "expression_heritage": "name", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + }, + "variable": { + "var_name": "name" + } + } + } + }, + { + "field": "overview", + "value": { + "expression": { + "expression_heritage": "overview", + "type": { + "element_type_name": "logicarecord980116590", + "rendered_type": "[{item: Str, quantity: Str}]", + "the_type": [ + { + "item": "Str", + "quantity": "Str" + } + ], + "type_id": 1 + }, + "variable": { + "var_name": "overview" + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_lists_test.l b/type_inference/research/integration_tests/typing_lists_test.l new file mode 100644 index 00000000..04593d85 --- /dev/null +++ b/type_inference/research/integration_tests/typing_lists_test.l @@ -0,0 +1,17 @@ +#!/usr/bin/python +# +# Copyright 2020 Google LLC +# +# 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. + +Test([1], a, b, c) :- a in [1, 2], T(b), a in b, c == ["hello"]; \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_lists_test.txt b/type_inference/research/integration_tests/typing_lists_test.txt new file mode 100644 index 00000000..45b04f86 --- /dev/null +++ b/type_inference/research/integration_tests/typing_lists_test.txt @@ -0,0 +1,270 @@ +[ + { + "body": { + "conjunction": { + "conjunct": [ + { + "inclusion": { + "element": { + "expression_heritage": "a", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "a" + } + }, + "list": { + "expression_heritage": "[1, 2]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 6 + } + }, + { + "expression_heritage": "2", + "literal": { + "the_number": { + "number": "2" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 7 + } + } + ] + } + }, + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 5 + } + } + } + }, + { + "predicate": { + "predicate_name": "T", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "b", + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 1 + }, + "variable": { + "var_name": "b" + } + } + } + } + ] + } + } + }, + { + "inclusion": { + "element": { + "expression_heritage": "a", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "a" + } + }, + "list": { + "expression_heritage": "b", + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 1 + }, + "variable": { + "var_name": "b" + } + } + } + }, + { + "unification": { + "left_hand_side": { + "expression_heritage": "c", + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 2 + }, + "variable": { + "var_name": "c" + } + }, + "right_hand_side": { + "expression_heritage": "[\"hello\"]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "\"hello\"", + "literal": { + "the_string": { + "the_string": "hello" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 9 + } + } + ] + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 8 + } + } + } + } + ] + } + }, + "full_text": "Test([1], a, b, c) :- a in [1, 2], T(b), a in b, c == [\"hello\"]", + "head": { + "predicate_name": "Test", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "[1]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 4 + } + } + ] + } + }, + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 3 + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "a", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "a" + } + } + } + }, + { + "field": 2, + "value": { + "expression": { + "expression_heritage": "b", + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 1 + }, + "variable": { + "var_name": "b" + } + } + } + }, + { + "field": 3, + "value": { + "expression": { + "expression_heritage": "c", + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 2 + }, + "variable": { + "var_name": "c" + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_nested_test.l b/type_inference/research/integration_tests/typing_nested_test.l new file mode 100644 index 00000000..25691ac2 --- /dev/null +++ b/type_inference/research/integration_tests/typing_nested_test.l @@ -0,0 +1,21 @@ +#!/usr/bin/python +# +# Copyright 2020 Google LLC +# +# 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. + +Test(y) :- + l = [{a: {b: {c: [1]}}}], + x in l, + c = x.a.b.c, + y in c; \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_nested_test.txt b/type_inference/research/integration_tests/typing_nested_test.txt new file mode 100644 index 00000000..ee98a255 --- /dev/null +++ b/type_inference/research/integration_tests/typing_nested_test.txt @@ -0,0 +1,396 @@ +[ + { + "body": { + "conjunction": { + "conjunct": [ + { + "predicate": { + "predicate_name": "=", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "l", + "type": { + "element_type_name": "logicarecord762067541", + "rendered_type": "[{a: {b: {c: [Num]}}}]", + "the_type": [ + { + "a": { + "b": { + "c": [ + "Num" + ] + } + } + } + ], + "type_id": 1 + }, + "variable": { + "var_name": "l" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "[{a: {b: {c: [1]}}}]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "{a: {b: {c: [1]}}}", + "record": { + "field_value": [ + { + "field": "a", + "value": { + "expression": { + "expression_heritage": "{b: {c: [1]}}", + "record": { + "field_value": [ + { + "field": "b", + "value": { + "expression": { + "expression_heritage": "{c: [1]}", + "record": { + "field_value": [ + { + "field": "c", + "value": { + "expression": { + "expression_heritage": "[1]", + "literal": { + "the_list": { + "element": [ + { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 9 + } + } + ] + } + }, + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 8 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{c: [Num]}", + "the_type": { + "c": [ + "Num" + ] + }, + "type_id": 7, + "type_name": "logicarecord137760342" + } + } + } + } + ] + }, + "type": { + "rendered_type": "{b: {c: [Num]}}", + "the_type": { + "b": { + "c": [ + "Num" + ] + } + }, + "type_id": 6, + "type_name": "logicarecord261470720" + } + } + } + } + ] + }, + "type": { + "rendered_type": "{a: {b: {c: [Num]}}}", + "the_type": { + "a": { + "b": { + "c": [ + "Num" + ] + } + } + }, + "type_id": 5, + "type_name": "logicarecord762067541" + } + } + ] + } + }, + "type": { + "element_type_name": "logicarecord762067541", + "rendered_type": "[{a: {b: {c: [Num]}}}]", + "the_type": [ + { + "a": { + "b": { + "c": [ + "Num" + ] + } + } + } + ], + "type_id": 4 + } + } + } + } + ] + } + } + }, + { + "inclusion": { + "element": { + "expression_heritage": "x", + "type": { + "rendered_type": "{a: {b: {c: [Num]}}}", + "the_type": { + "a": { + "b": { + "c": [ + "Num" + ] + } + } + }, + "type_id": 2, + "type_name": "logicarecord762067541" + }, + "variable": { + "var_name": "x" + } + }, + "list": { + "expression_heritage": "l", + "type": { + "element_type_name": "logicarecord762067541", + "rendered_type": "[{a: {b: {c: [Num]}}}]", + "the_type": [ + { + "a": { + "b": { + "c": [ + "Num" + ] + } + } + } + ], + "type_id": 1 + }, + "variable": { + "var_name": "l" + } + } + } + }, + { + "predicate": { + "predicate_name": "=", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "c", + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 3 + }, + "variable": { + "var_name": "c" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "x.a.b.c", + "subscript": { + "record": { + "expression_heritage": "x.a.b", + "subscript": { + "record": { + "expression_heritage": "x.a", + "subscript": { + "record": { + "expression_heritage": "x", + "type": { + "rendered_type": "{a: {b: {c: [Num]}}}", + "the_type": { + "a": { + "b": { + "c": [ + "Num" + ] + } + } + }, + "type_id": 2, + "type_name": "logicarecord762067541" + }, + "variable": { + "var_name": "x" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "a" + } + } + } + }, + "type": { + "rendered_type": "{b: {c: [Num]}}", + "the_type": { + "b": { + "c": [ + "Num" + ] + } + }, + "type_id": 12, + "type_name": "logicarecord261470720" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "b" + } + } + } + }, + "type": { + "rendered_type": "{c: [Num]}", + "the_type": { + "c": [ + "Num" + ] + }, + "type_id": 11, + "type_name": "logicarecord137760342" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "c" + } + } + } + }, + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 10 + } + } + } + } + ] + } + } + }, + { + "inclusion": { + "element": { + "expression_heritage": "y", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "y" + } + }, + "list": { + "expression_heritage": "c", + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 3 + }, + "variable": { + "var_name": "c" + } + } + } + } + ] + } + }, + "full_text": "Test(y) :-\n l = [{a: {b: {c: [1]}}}],\n x in l,\n c = x.a.b.c,\n y in c", + "head": { + "predicate_name": "Test", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "y", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "y" + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/integration_tests/typing_palindrome_puzzle_test.l b/type_inference/research/integration_tests/typing_palindrome_puzzle_test.l new file mode 100644 index 00000000..6284bc63 --- /dev/null +++ b/type_inference/research/integration_tests/typing_palindrome_puzzle_test.l @@ -0,0 +1,33 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + +@Engine("psql"); + +Word("abbacdcaaaxx"); + +Char(i) = Substr(word, i + 1, 1) :- Word(word), i in Range(Length(word)); + +@Ground(Palindrome); +Palindrome(i, i) = Char(i); +Palindrome(i, i + 1) = Char(i) ++ Char(i + 1) :- Char(i) == Char(i + 1); +Palindrome(i - 1, j + 1) = Char(i - 1) ++ Palindrome(i, j) ++ Char(j + 1) :- Palindrome(i, j), Char(i - 1) == Char(j + 1); + +@Ground(Path); +Path(i, j) = {path: [Palindrome(i, j)]} distinct; +Path(i, k) = {path: ArrayConcat(Path(i, j).path, Path(j + 1, k).path)} distinct; + +ShortestPath() ArgMin= path -> Size(path.path) :- path == Path(); + diff --git a/type_inference/research/integration_tests/typing_palindrome_puzzle_test.txt b/type_inference/research/integration_tests/typing_palindrome_puzzle_test.txt new file mode 100644 index 00000000..3cc9fac1 --- /dev/null +++ b/type_inference/research/integration_tests/typing_palindrome_puzzle_test.txt @@ -0,0 +1,2008 @@ +[ + { + "full_text": "@Engine(\"psql\")", + "head": { + "predicate_name": "@Engine", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "\"psql\"", + "literal": { + "the_string": { + "the_string": "psql" + } + } + } + } + } + ] + } + } + }, + { + "full_text": "Word(\"abbacdcaaaxx\")", + "head": { + "predicate_name": "Word", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "\"abbacdcaaaxx\"", + "literal": { + "the_string": { + "the_string": "abbacdcaaaxx" + } + }, + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 0 + } + } + } + } + ] + } + } + }, + { + "body": { + "conjunction": { + "conjunct": [ + { + "predicate": { + "predicate_name": "Word", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "word", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 1 + }, + "variable": { + "var_name": "word" + } + } + } + } + ] + } + } + }, + { + "inclusion": { + "element": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + }, + "list": { + "call": { + "predicate_name": "Range", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "Length", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "word", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 1 + }, + "variable": { + "var_name": "word" + } + } + } + } + ] + } + }, + "expression_heritage": "Length(word)", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 7 + } + } + } + } + ] + } + }, + "expression_heritage": "Range(Length(word))", + "type": { + "element_type_name": "numeric", + "rendered_type": "[Num]", + "the_type": [ + "Num" + ], + "type_id": 6 + } + } + } + } + ] + } + }, + "full_text": "Char(i) = Substr(word, i + 1, 1) :- Word(word), i in Range(Length(word))", + "head": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "call": { + "predicate_name": "Substr", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "word", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 1 + }, + "variable": { + "var_name": "word" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 4 + } + } + } + } + ] + } + }, + "expression_heritage": "i + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 3 + } + } + } + }, + { + "field": 2, + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + } + } + } + } + ] + } + }, + "expression_heritage": "Substr(word, i + 1, 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 2 + } + } + } + } + ] + } + } + }, + { + "full_text": "@Ground(Palindrome)", + "head": { + "predicate_name": "@Ground", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "Palindrome", + "literal": { + "the_predicate": { + "predicate_name": "Palindrome" + } + } + } + } + } + ] + } + } + }, + { + "full_text": "Palindrome(i, i) = Char(i)", + "head": { + "predicate_name": "Palindrome", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 1 + } + } + } + } + ] + } + } + }, + { + "body": { + "conjunction": { + "conjunct": [ + { + "unification": { + "left_hand_side": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 8 + } + }, + "right_hand_side": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 11 + } + } + } + } + ] + } + }, + "expression_heritage": "i + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 10 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i + 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 9 + } + } + } + } + ] + } + }, + "full_text": "Palindrome(i, i + 1) = Char(i) ++ Char(i + 1) :- Char(i) == Char(i + 1)", + "head": { + "predicate_name": "Palindrome", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 2 + } + } + } + } + ] + } + }, + "expression_heritage": "i + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "call": { + "predicate_name": "++", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 4 + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 7 + } + } + } + } + ] + } + }, + "expression_heritage": "i + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 6 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i + 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 5 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i) ++ Char(i + 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 3 + } + } + } + } + ] + } + } + }, + { + "body": { + "conjunction": { + "conjunct": [ + { + "predicate": { + "predicate_name": "Palindrome", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "j" + } + } + } + } + ] + } + } + }, + { + "unification": { + "left_hand_side": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "-", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 18 + } + } + } + } + ] + } + }, + "expression_heritage": "i - 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 17 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i - 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 15 + } + }, + "right_hand_side": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "j" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 20 + } + } + } + } + ] + } + }, + "expression_heritage": "j + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 19 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(j + 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 16 + } + } + } + } + ] + } + }, + "full_text": "Palindrome(i - 1, j + 1) = Char(i - 1) ++ Palindrome(i, j) ++ Char(j + 1) :- Palindrome(i, j), Char(i - 1) == Char(j + 1)", + "head": { + "predicate_name": "Palindrome", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "-", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 3 + } + } + } + } + ] + } + }, + "expression_heritage": "i - 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 2 + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "j" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 5 + } + } + } + } + ] + } + }, + "expression_heritage": "j + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 4 + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "call": { + "predicate_name": "++", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "call": { + "predicate_name": "++", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "-", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 10 + } + } + } + } + ] + } + }, + "expression_heritage": "i - 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 9 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i - 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 8 + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "call": { + "predicate_name": "Palindrome", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "j" + } + } + } + } + ] + } + }, + "expression_heritage": "Palindrome(i, j)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 11 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i - 1) ++ Palindrome(i, j)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 7 + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "call": { + "predicate_name": "Char", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "j" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 14 + } + } + } + } + ] + } + }, + "expression_heritage": "j + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 13 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(j + 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 12 + } + } + } + } + ] + } + }, + "expression_heritage": "Char(i - 1) ++ Palindrome(i, j) ++ Char(j + 1)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 6 + } + } + } + } + ] + } + } + }, + { + "full_text": "@Ground(Path)", + "head": { + "predicate_name": "@Ground", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "Path", + "literal": { + "the_predicate": { + "predicate_name": "Path" + } + } + } + } + } + ] + } + } + }, + { + "full_text": "Path(i, j) = {path: [Palindrome(i, j)]} distinct", + "head": { + "predicate_name": "Path_MultBodyAggAux", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "j" + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "expression_heritage": "{path: [Palindrome(i, j)]}", + "record": { + "field_value": [ + { + "field": "path", + "value": { + "expression": { + "expression_heritage": "[Palindrome(i, j)]", + "literal": { + "the_list": { + "element": [ + { + "call": { + "predicate_name": "Palindrome", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "j" + } + } + } + } + ] + } + }, + "expression_heritage": "Palindrome(i, j)", + "type": { + "rendered_type": "Str", + "the_type": "Str", + "type_id": 4 + } + } + ] + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 3 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 2, + "type_name": "logicarecord808144452" + } + } + } + } + ] + } + } + }, + { + "full_text": "Path(i, k) = {path: ArrayConcat(Path(i, j).path, Path(j + 1, k).path)} distinct", + "head": { + "predicate_name": "Path_MultBodyAggAux", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "k", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "k" + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "expression_heritage": "{path: ArrayConcat(Path(i, j).path, Path(j + 1, k).path)}", + "record": { + "field_value": [ + { + "field": "path", + "value": { + "expression": { + "call": { + "predicate_name": "ArrayConcat", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "Path(i, j).path", + "subscript": { + "record": { + "call": { + "predicate_name": "Path", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "i", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": "i" + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 2 + }, + "variable": { + "var_name": "j" + } + } + } + } + ] + } + }, + "expression_heritage": "Path(i, j)", + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 6, + "type_name": "logicarecord808144452" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "path" + } + } + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 5 + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "Path(j + 1, k).path", + "subscript": { + "record": { + "call": { + "predicate_name": "Path", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "+", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "j", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 2 + }, + "variable": { + "var_name": "j" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "expression_heritage": "1", + "literal": { + "the_number": { + "number": "1" + } + }, + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 10 + } + } + } + } + ] + } + }, + "expression_heritage": "j + 1", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 9 + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "expression_heritage": "k", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": "k" + } + } + } + } + ] + } + }, + "expression_heritage": "Path(j + 1, k)", + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 8, + "type_name": "logicarecord808144452" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "path" + } + } + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 7 + } + } + } + } + ] + } + }, + "expression_heritage": "ArrayConcat(Path(i, j).path, Path(j + 1, k).path)", + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 4 + } + } + } + } + ] + }, + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 3, + "type_name": "logicarecord808144452" + } + } + } + } + ] + } + } + }, + { + "body": { + "conjunction": { + "conjunct": [ + { + "unification": { + "left_hand_side": { + "expression_heritage": "path", + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 0, + "type_name": "logicarecord808144452" + }, + "variable": { + "var_name": "path" + } + }, + "right_hand_side": { + "call": { + "predicate_name": "Path", + "record": { + "field_value": [] + } + }, + "expression_heritage": "Path()", + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 5, + "type_name": "logicarecord808144452" + } + } + } + } + ] + } + }, + "distinct_denoted": true, + "full_text": "ShortestPath() ArgMin= path -> Size(path.path) :- path == Path()", + "head": { + "predicate_name": "ShortestPath", + "record": { + "field_value": [ + { + "field": "logica_value", + "value": { + "aggregation": { + "expression": { + "call": { + "predicate_name": "ArgMin", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "call": { + "predicate_name": "->", + "record": { + "field_value": [ + { + "field": "left", + "value": { + "expression": { + "expression_heritage": "path", + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 0, + "type_name": "logicarecord808144452" + }, + "variable": { + "var_name": "path" + } + } + } + }, + { + "field": "right", + "value": { + "expression": { + "call": { + "predicate_name": "Size", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "expression_heritage": "path.path", + "subscript": { + "record": { + "expression_heritage": "path", + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 0, + "type_name": "logicarecord808144452" + }, + "variable": { + "var_name": "path" + } + }, + "subscript": { + "literal": { + "the_symbol": { + "symbol": "path" + } + } + } + }, + "type": { + "element_type_name": "text", + "rendered_type": "[Str]", + "the_type": [ + "Str" + ], + "type_id": 4 + } + } + } + } + ] + } + }, + "expression_heritage": "Size(path.path)", + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 3 + } + } + } + } + ] + } + }, + "expression_heritage": "path -> Size(path.path)", + "type": { + "rendered_type": "{arg: {path: [Str]}, value: Num}", + "the_type": { + "arg": { + "path": [ + "Str" + ] + }, + "value": "Num" + }, + "type_id": 2, + "type_name": "logicarecord659526184" + } + } + } + } + ] + } + }, + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 1, + "type_name": "logicarecord808144452" + } + } + } + } + } + ] + } + } + }, + { + "body": { + "conjunction": { + "conjunct": [ + { + "predicate": { + "predicate_name": "Path_MultBodyAggAux", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": 0 + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": 1 + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 2, + "type_name": "logicarecord808144452" + }, + "variable": { + "var_name": "logica_value" + } + } + } + } + ] + } + } + } + ] + } + }, + "distinct_denoted": true, + "full_text": "Path(i, k) = {path: ArrayConcat(Path(i, j).path, Path(j + 1, k).path)} distinct", + "head": { + "predicate_name": "Path", + "record": { + "field_value": [ + { + "field": 0, + "value": { + "expression": { + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 0 + }, + "variable": { + "var_name": 0 + } + } + } + }, + { + "field": 1, + "value": { + "expression": { + "type": { + "rendered_type": "Num", + "the_type": "Num", + "type_id": 1 + }, + "variable": { + "var_name": 1 + } + } + } + }, + { + "field": "logica_value", + "value": { + "expression": { + "type": { + "rendered_type": "{path: [Str]}", + "the_type": { + "path": [ + "Str" + ] + }, + "type_id": 2, + "type_name": "logicarecord808144452" + }, + "variable": { + "var_name": "logica_value" + } + } + } + } + ] + } + } + } +] \ No newline at end of file diff --git a/type_inference/research/reference_algebra.py b/type_inference/research/reference_algebra.py new file mode 100644 index 00000000..25594974 --- /dev/null +++ b/type_inference/research/reference_algebra.py @@ -0,0 +1,383 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + +if '.' not in __package__: + from common import color +else: + try: + from ...common import color + except: + from common import color + + +class OpenRecord(dict): + def __str__(self): + return '{%s, ...}' % str(dict(self))[1:-1] + def __repr__(self): + return str(self) + + +class ClosedRecord(dict): + def __str__(self): + return str(dict(self)) + def __repr__(self): + return str(self) + + +class BadType(tuple): + def __str__(self): + if (isinstance(self[0], dict) and + isinstance(self[1], dict)): + if isinstance(self[0], + ClosedRecord): + a, b = self + else: + b, a = self + elif self[1] == 'Singular': + b, a = self + else: + a, b = self + + colored_t1 = color.Format('{warning}{t}{end}', + args_dict={'t': RenderType(a)}) + colored_t2 = color.Format('{warning}{t}{end}', + args_dict={'t': RenderType(b)}) + + if (isinstance(a, ClosedRecord) and + isinstance(b, OpenRecord) and + list(b)[0] not in a.keys()): + colored_e = color.Format( + '{warning}{t}{end}', args_dict={'t': RenderType(list(b)[0])}) + return ( + f'is a record {colored_t1} and it does not have ' + + f'field {colored_e}, which is addressed.' + ) + if a == 'Singular': + assert isinstance(b, list), 'Fatally incorrect singular error: %s' % b + return ( + f'belongs to a list, but is implied to be {colored_t2}. ' + f'Logica has to follow existing DB practice (Posgres, BigQuery) ' + f'and disallow lists to be elements of lists. This includes ' + f'ArgMaxK and ArgMinK aggregations, as they build lists. ' + f'Kindly wrap your inner list into a single field ' + f'record.' + ) + + return ( + f'is implied to be {colored_t1} and ' + + f'simultaneously {colored_t2}, which is impossible.') + def __repr__(self): + return str(self) + +class TypeReference: + def __init__(self, target): + self.target = target + + def WeMustGoDeeper(self): + return isinstance(self.target, TypeReference) + + def Target(self): + result = self + while result.WeMustGoDeeper(): + result = result.target + return result.target + + def TargetTypeClassName(self): + target = self.Target() + return type(target).__name__ + + @classmethod + def To(cls, target): + if isinstance(target, TypeReference): + return target + return TypeReference(target) + + def IsBadType(self): + return isinstance(self.Target(), BadType) + + def __str__(self): + return str(self.target) + '@' + hex(id(self)) + + def __repr__(self): + return str(self) + + def CloseRecord(self): + a = self + while a.WeMustGoDeeper(): + a = a.target + if isinstance(a.target, BadType): + return + assert isinstance(a.target, dict), a.target + a.target = ClosedRecord(a.target) + +def RenderType(t): + if isinstance(t, str): + return t + if isinstance(t, list): + return '[%s]' % RenderType(t[0]) + if isinstance(t, dict): + return '{%s}' % ', '.join('%s: %s' % (k, RenderType(v)) + for k, v in sorted(t.items())) + if isinstance(t, tuple): + return '(%s != %s)' % (RenderType(t[0]), RenderType(t[1])) + assert False, type(t) + +def ConcreteType(t): + if isinstance(t, TypeReference): + return t.Target() + assert (isinstance(t, BadType) or + isinstance(t, list) or + isinstance(t, dict) or + isinstance(t, str)) + return t + +def VeryConcreteType(t, upward=None): + upward = upward or set() + if id(t) in upward: + return BadType(('...', '...')) + else: + upward = upward | set([id(t)]) + + c = ConcreteType(t) + if isinstance(c, BadType): + return BadType(VeryConcreteType(e, upward) for e in c) + if isinstance(c, str): + return c + + if isinstance(c, list): + return [VeryConcreteType(e, upward) for e in c] + + if isinstance(c, dict): + return type(c)({f: VeryConcreteType(v, upward) for f, v in c.items()}) + + assert False + + +def IsFullyDefined(t): + if t == 'Any': + return False + if t == 'Singular': + return False + if isinstance(t, str): + return True + if isinstance(t, BadType): + return False + if isinstance(t, list): + [e] = t + return IsFullyDefined(e) + if isinstance(t, dict): + return all(IsFullyDefined(v) for v in t.values()) + assert False + + +def Rank(x): + """Rank of the type, arbitrary order for sorting.""" + x = ConcreteType(x) + if isinstance(x, BadType): # Tuple means error. + return -1 + if x == 'Any': + return 0 + if x == 'Singular': + return 1 + if x == 'Num': + return 2 + if x == 'Str': + return 3 + if x == 'Bool': + return 4 + if isinstance(x, list): + return 5 + if isinstance(x, OpenRecord): + return 6 + if isinstance(x, ClosedRecord): + return 7 + assert False, 'Bad type: %s' % x + + +def Incompatible(a, b): + return BadType((a, b)) + + +def Unify(a, b): + """Unifies type reference a with type reference b.""" + original_a = a + original_b = b + while a.WeMustGoDeeper(): + a = a.target + while b.WeMustGoDeeper(): + b = b.target + if original_a != a: + original_a.target = a + if original_b != b: + original_b.target = b + if id(a) == id(b): + return + assert isinstance(a, TypeReference) + assert isinstance(b, TypeReference) + concrete_a = ConcreteType(a) + concrete_b = ConcreteType(b) + + if isinstance(concrete_a, BadType) or isinstance(concrete_b, BadType): + return # Do nothing. + + if Rank(concrete_a) > Rank(concrete_b): + a, b = b, a + concrete_a, concrete_b = concrete_b, concrete_a + + if concrete_a == 'Any': + a.target = b + return + + if concrete_a == 'Singular': + if isinstance(concrete_b, list): + a.target, b.target = ( + Incompatible(a.target, b.target), + Incompatible(b.target, a.target)) + return + a.target = b + return + + if concrete_a in ('Num', 'Str', 'Bool'): + if concrete_a == concrete_b: + return # It's all fine. + # Type error: a is incompatible with b. + a.target, b.target = ( + Incompatible(a.target, b.target), + Incompatible(b.target, a.target)) + return + + if isinstance(concrete_a, list): + if isinstance(concrete_b, list): + a_element, b_element = concrete_a + concrete_b + a_element = TypeReference.To(a_element) + b_element = TypeReference.To(b_element) + Unify(a_element, b_element) + # TODO: Make this correct. + if a_element.TargetTypeClassName() == 'BadType': + a.target, b.target = ( + Incompatible(a.target, b.target), + Incompatible(b.target, a.target)) + return + a.target = [a_element] + b.target = [b_element] + return + a.target, b.target = ( + Incompatible(a.target, b.target), + Incompatible(b.target, a.target)) + + return + + if isinstance(concrete_a, OpenRecord): + if isinstance(concrete_b, OpenRecord): + UnifyFriendlyRecords(a, b, OpenRecord) + return + if isinstance(concrete_b, ClosedRecord): + if set(concrete_a) <= set(concrete_b): + UnifyFriendlyRecords(a, b, ClosedRecord) + return + a.target, b.target = ( + Incompatible(a.target, b.target), + Incompatible(b.target, a.target)) + return + assert False + + if isinstance(concrete_a, ClosedRecord): + if isinstance(concrete_b, ClosedRecord): + if set(concrete_a) == set(concrete_b): + UnifyFriendlyRecords(a, b, ClosedRecord) + return + a.target = Incompatible(a.target, b.target) + b.target = Incompatible(b.target, a.target) + return + assert False + assert False, (a, type(a)) + + +def UnifyFriendlyRecords(a, b, record_type): + """Intersecting records assuming that their fields are compatible.""" + concrete_a = ConcreteType(a) + concrete_b = ConcreteType(b) + result = {} + for f in set(concrete_a) | set(concrete_b): + x = TypeReference.To('Any') + if f in concrete_a: + Unify(x, TypeReference.To(concrete_a[f])) + if f in concrete_b: + Unify(x, TypeReference.To(concrete_b[f])) + if x.TargetTypeClassName() == 'BadType': # Ooops, field type error. + a.target = Incompatible(a, b) + b.target = Incompatible(b, a) + result[f] = x + a.target = TypeReference(record_type(result)) + b.target = a.target + + +def UnifyListElement(a_list, b_element): + """Analysis of expression `b in a`.""" + Unify(b_element, TypeReference.To('Singular')) + b = TypeReference([b_element]) + Unify(a_list, b) + + +def UnifyRecordField(a_record, field_name, b_field_value): + """Analysis of expresson `a.f = b`.""" + b = TypeReference(OpenRecord({field_name: b_field_value})) + Unify(a_record, b) + +class TypeStructureCopier: + def __init__(self): + self.id_to_reference = {} + + def CopyConcreteOrReferenceType(self, t): + if isinstance(t, TypeReference): + return self.CopyTypeReference(t) + return self.CopyConcreteType(t) + + def CopyConcreteType(self, t): + if isinstance(t, str): + return t + if isinstance(t, list): + return [self.CopyConcreteOrReferenceType(e) for e in t] + if isinstance(t, dict): + c = type(t) + return c({k: self.CopyConcreteOrReferenceType(v) for k, v in t.items()}) + if isinstance(t, BadType): + return BadType((self.CopyConcreteOrReferenceType(t[0]), + self.CopyConcreteOrReferenceType(t[1]))) + assert False, (t, type(t)) + + def CopyTypeReference(self, t): + if id(t) not in self.id_to_reference: + target = self.CopyConcreteOrReferenceType(t.target) + n = TypeReference(target) + self.id_to_reference[id(t)] = n + return self.id_to_reference[id(t)] + +def Revive(t): + if isinstance(t, str): + return TypeReference(t) + if isinstance(t, dict): + def ReviveKey(k): + if k in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']: + return int(k) + return k + return TypeReference(OpenRecord( + {ReviveKey(k): Revive(v) for k, v in t.items()})) + if isinstance(t, list): + return TypeReference(list(map(Revive, t))) + if isinstance(t, BadType): + return TypeReference(BadType(map(Revive, t))) + assert False, [type(t), t] \ No newline at end of file diff --git a/type_inference/research/types_of_builtins.py b/type_inference/research/types_of_builtins.py new file mode 100644 index 00000000..65c501e1 --- /dev/null +++ b/type_inference/research/types_of_builtins.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# +# Copyright 2023 Google LLC +# +# 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. + +if '.' not in __package__: + from type_inference.research import reference_algebra +else: + from ..research import reference_algebra + +def TypesOfBultins(): + x = reference_algebra.TypeReference('Any') + y = reference_algebra.TypeReference('Any') + # Special X that ends up singular in SQLite. + special_x = reference_algebra.TypeReference('Any') + list_of_e = reference_algebra.TypeReference('Any') + e = reference_algebra.TypeReference('Singular') + reference_algebra.UnifyListElement(list_of_e, e) + + types_of_predicate = { + 'Aggr': { + 0: x, + 'logica_value': x + }, + '==': { + 'left': x, + 'right': x, + 'logica_value': 'Bool' + }, + '=': { + 'left': x, + 'right': x, + 'logica_value': x + }, + '~': { + 'left': x, + 'right': x + }, + '++': { + 'left': 'Str', + 'right': 'Str', + 'logica_value': 'Str' + }, + '+': { + 'left': 'Num', + 'right': 'Num', + 'logica_value': 'Num' + }, + '*': { + 'left': 'Num', + 'right': 'Num', + 'logica_value': 'Num' + }, + '^': { + 'left': 'Num', + 'right': 'Num', + 'logica_value': 'Num' + }, + 'Num': { + 0: 'Num', + 'logica_value': 'Num' + }, + 'Str': { + 0: 'Str', + 'logica_value': 'Str' + }, + 'Agg+': { + 0: 'Num', + 'logica_value': 'Num' + }, + 'List': { + 0: e, + 'logica_value': list_of_e + }, + '->': { + 'left': x, + 'right': y, + 'logica_value': reference_algebra.ClosedRecord({'arg': x, 'value': y}) + }, + 'ArgMin': { + 0: reference_algebra.ClosedRecord({'arg': special_x, 'value': y}), + 'logica_value': special_x + }, + 'ArgMax': { + 0: reference_algebra.ClosedRecord({'arg': special_x, 'value': y}), + 'logica_value': special_x + }, + 'ArgMinK': { + 0: reference_algebra.ClosedRecord({'arg': e, 'value': y}), + 1: 'Num', + 'logica_value': [e] + }, + 'ArgMaxK': { + 0: reference_algebra.ClosedRecord({'arg': e, 'value': y}), + 1: 'Num', + 'logica_value': [e] + }, + 'Range': { + 0: 'Num', + 'logica_value': ['Num'] + }, + 'Length': { + 0: 'Str', + 'logica_value': 'Num' + }, + 'Size': { + 0: ['Singular'], + 'logica_value': 'Num' + }, + '-': { + 0: 'Num', + 'left': 'Num', + 'right': 'Num', + 'logica_value': 'Num' + }, + 'Min': { + 0: x, + 'logica_value': x + }, + 'Max': { + 0: x, + 'logica_value': y + }, + 'Array': { + 0: reference_algebra.ClosedRecord({'arg': x, 'value': e}), + 'logica_value': e + }, + 'ValueOfUnnested': { + 0: x, + 'logica_value': x + }, + 'RecordAsJson': { + 0: reference_algebra.OpenRecord({}), + 'logica_value': 'Str' + }, + '>': { + 'left': x, + 'right': x, + 'logica_value': 'Bool' + }, + 'ArrayConcat': { + 0: [e], + 1: [e], + 'logica_value': [e] + }, + 'Substr': { + 0: 'Str', + 1: 'Num', + 2: 'Num', + 'logica_value': 'Str' + }, + 'Fingerprint': { + 0: 'Str', + 'logica_value': 'Num' + }, + 'Abs': { + 0: 'Num', + 'logica_value': 'Num' + }, + '!': { + 0: 'Bool', + 'logica_value': 'Bool' + }, + '||': { + 'left': 'Bool', + 'right': 'Bool', + 'logica_value': 'Bool' + }, + 'IsNull': { + 0: 'Any', + 'logica_value': 'Bool' + }, + 'ToString': { + 0: 'Any', + 'logica_value': 'Str' + }, + 'ToInt64': { + 0: 'Any', + 'logica_value': 'Num' + }, + 'ToFloat64': { + 0: 'Any', + 'logica_value': 'Num' + }, + 'AnyValue': { + 0: x, + 'logica_value': x + }, + 'Format': { + 0: 'Str', + 1: 'Any', 2: 'Any', 3: 'Any', 4: 'Any', 5: 'Any', 6: 'Any', + 'logica_value': 'Str' + }, + 'Element': { + 0: list_of_e, + 1: 'Num', + 'logica_value': e + } + } + types_of_predicate['<'] = types_of_predicate['<='] = types_of_predicate['>='] = types_of_predicate['>'] + types_of_predicate['Sin'] = types_of_predicate['Cos'] = types_of_predicate['Log' + ] = types_of_predicate['Exp'] = types_of_predicate['Abs'] + types_of_predicate['%'] = types_of_predicate['/'] = types_of_predicate['*'] + types_of_predicate['&&'] = types_of_predicate['||'] + return { + p: {k: reference_algebra.TypeReference(v) + for k, v in types.items()} + for p, types in types_of_predicate.items() + }