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('
' % (
+ 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?
- 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.
-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": [
+ ""
+ ],
+ "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",
+ " col0 | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " Hello world! | \n",
+ "
\n",
+ " \n",
+ "
\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()
+ }