Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache for Definitions #507

Merged
merged 4 commits into from
Sep 6, 2016
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 124 additions & 42 deletions mathics/core/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import re
import bisect

from mathics.core.expression import Expression, Symbol, String, fully_qualified_symbol_name
from collections import defaultdict

from mathics.core.expression import Expression, Symbol, String, fully_qualified_symbol_name, strip_context
from mathics.core.characters import letters, letterlikes


Expand Down Expand Up @@ -44,6 +46,10 @@ def __init__(self, add_builtin=False, builtin_filename=None):
self.builtin = {}
self.user = {}

self.definitions_cache = {}
self.lookup_cache = {}
self.proxy = defaultdict(set)

if add_builtin:
from mathics.builtin import modules, contribute
from mathics.core.evaluation import Evaluation
Expand Down Expand Up @@ -81,6 +87,41 @@ def __init__(self, add_builtin=False, builtin_filename=None):
raise ValueError("autoload defined %s." % name)
self.builtin.update(self.user)
self.user = {}
self.clear_cache()

def clear_cache(self, name=None):
# the definitions cache (self.definitions_cache) caches (incomplete and complete) names -> Definition(),
# e.g. "xy" -> d and "MyContext`xy" -> d. we need to clear this cache if a Definition() changes (which
# would happen if a Definition is combined from a builtin and a user definition and some content in the
# user definition is updated) or if the lookup rules change and we could end up at a completely different
# Definition.

# the lookup cache (self.lookup_cache) caches what lookup_name() does. we only need to update this if some
# change happens that might change the result lookup_name() calculates. we do not need to change it if a
# Definition() changes.

# self.proxy keeps track of all the names we cache. if we need to clear the caches for only one name, e.g.
# 'MySymbol', then we need to be able to look up all the entries that might be related to it, e.g. 'MySymbol',
# 'A`MySymbol', 'C`A`MySymbol', and so on. proxy identifies symbols using their stripped name and thus might
# give us symbols in other contexts that are actually not affected. still, this is a safe solution.

if name is None:
self.definitions_cache = {}
self.lookup_cache = {}
self.proxy = defaultdict(set)
else:
definitions_cache = self.definitions_cache
lookup_cache = self.lookup_cache
tail = strip_context(name)
for k in self.proxy.pop(tail, []):
definitions_cache.pop(k, None)
lookup_cache.pop(k, None)

def clear_definitions_cache(self, name):
definitions_cache = self.definitions_cache
tail = strip_context(name)
for k in self.proxy.pop(tail, []):
definitions_cache.pop(k, None)

def get_current_context(self):
# It's crucial to specify System` in this get_ownvalue() call,
Expand All @@ -102,13 +143,15 @@ def get_context_path(self):
def set_current_context(self, context):
assert isinstance(context, six.string_types)
self.set_ownvalue('System`$Context', String(context))
self.clear_cache()

def set_context_path(self, context_path):
assert isinstance(context_path, list)
assert all([isinstance(c, six.string_types) for c in context_path])
self.set_ownvalue('System`$ContextPath',
Expression('System`List',
*[String(c) for c in context_path]))
self.clear_cache()

def get_builtin_names(self):
return set(self.builtin)
Expand Down Expand Up @@ -184,6 +227,10 @@ def lookup_name(self, name):
- Otherwise, it's a new symbol in $Context.
"""

cached = self.lookup_cache.get(name, None)
if cached is not None:
return cached

assert isinstance(name, six.string_types)

# Bail out if the name we're being asked to look up is already
Expand Down Expand Up @@ -224,49 +271,63 @@ def have_definition(self, name):
return self.get_definition(name, only_if_exists=True) is not None

def get_definition(self, name, only_if_exists=False):
definition = self.definitions_cache.get(name, None)
if definition is not None:
return definition

original_name = name
name = self.lookup_name(name)
user = self.user.get(name, None)
builtin = self.builtin.get(name, None)

if user is None and builtin is None:
return None if only_if_exists else Definition(name=name)
if builtin is None:
return user
if user is None:
return builtin

if user:
attributes = user.attributes
elif builtin:
attributes = builtin.attributes
definition = None
elif builtin is None:
definition = user
elif user is None:
definition = builtin
else:
attributes = set()
if not user:
user = Definition(name=name)
if not builtin:
builtin = Definition(name=name)
options = builtin.options.copy()
options.update(user.options)
formatvalues = builtin.formatvalues.copy()
for form, rules in six.iteritems(user.formatvalues):
if form in formatvalues:
formatvalues[form].extend(rules)
if user:
attributes = user.attributes
elif builtin:
attributes = builtin.attributes
else:
formatvalues[form] = rules

return Definition(name=name,
ownvalues=user.ownvalues + builtin.ownvalues,
downvalues=user.downvalues + builtin.downvalues,
subvalues=user.subvalues + builtin.subvalues,
upvalues=user.upvalues + builtin.upvalues,
formatvalues=formatvalues,
messages=user.messages + builtin.messages,
attributes=attributes,
options=options,
nvalues=user.nvalues + builtin.nvalues,
defaultvalues=user.defaultvalues +
builtin.defaultvalues,
)
attributes = set()
if not user:
user = Definition(name=name)
if not builtin:
builtin = Definition(name=name)
options = builtin.options.copy()
options.update(user.options)
formatvalues = builtin.formatvalues.copy()
for form, rules in six.iteritems(user.formatvalues):
if form in formatvalues:
formatvalues[form].extend(rules)
else:
formatvalues[form] = rules

definition = Definition(name=name,
ownvalues=user.ownvalues + builtin.ownvalues,
downvalues=user.downvalues + builtin.downvalues,
subvalues=user.subvalues + builtin.subvalues,
upvalues=user.upvalues + builtin.upvalues,
formatvalues=formatvalues,
messages=user.messages + builtin.messages,
attributes=attributes,
options=options,
nvalues=user.nvalues + builtin.nvalues,
defaultvalues=user.defaultvalues +
builtin.defaultvalues,
)

if definition is not None:
self.proxy[strip_context(original_name)].add(original_name)
self.definitions_cache[original_name] = definition
self.lookup_cache[original_name] = name
elif not only_if_exists:
definition = Definition(name=name)

return definition

def get_attributes(self, name):
return self.get_definition(name).attributes
Expand Down Expand Up @@ -319,35 +380,45 @@ def get_user_definition(self, name, create=True):
else:
attributes = set()
self.user[name] = Definition(name=name, attributes=attributes)
self.clear_cache(name)
return self.user[name]

def reset_user_definition(self, name):
assert not isinstance(name, Symbol)
del self.user[self.lookup_name(name)]
fullname = self.lookup_name(name)
del self.user[fullname]
self.clear_cache(fullname)

def add_user_definition(self, name, definition):
assert not isinstance(name, Symbol)
self.user[self.lookup_name(name)] = definition
fullname = self.lookup_name(name)
self.user[fullname] = definition
self.clear_cache(fullname)

def set_attribute(self, name, attribute):
definition = self.get_user_definition(self.lookup_name(name))
definition.attributes.add(attribute)
self.clear_definitions_cache(name)

def set_attributes(self, name, attributes):
definition = self.get_user_definition(self.lookup_name(name))
definition.attributes = set(attributes)
self.clear_definitions_cache(name)

def clear_attribute(self, name, attribute):
definition = self.get_user_definition(self.lookup_name(name))
if attribute in definition.attributes:
definition.attributes.remove(attribute)
self.clear_definitions_cache(name)

def add_rule(self, name, rule, position=None):
name = self.lookup_name(name)
if position is None:
return self.get_user_definition(name).add_rule(rule)
result = self.get_user_definition(name).add_rule(rule)
else:
return self.get_user_definition(name).add_rule_at(rule, position)
result = self.get_user_definition(name).add_rule_at(rule, position)
self.clear_definitions_cache(name)
return result

def add_format(self, name, rule, form=''):
definition = self.get_user_definition(self.lookup_name(name))
Expand All @@ -359,29 +430,35 @@ def add_format(self, name, rule, form=''):
if form not in definition.formatvalues:
definition.formatvalues[form] = []
insert_rule(definition.formatvalues[form], rule)
self.clear_definitions_cache(name)

def add_nvalue(self, name, rule):
definition = self.get_user_definition(self.lookup_name(name))
definition.add_rule_at(rule, 'n')
self.clear_definitions_cache(name)

def add_default(self, name, rule):
definition = self.get_user_definition(self.lookup_name(name))
definition.add_rule_at(rule, 'default')
self.clear_definitions_cache(name)

def add_message(self, name, rule):
definition = self.get_user_definition(self.lookup_name(name))
definition.add_rule_at(rule, 'messages')
self.clear_definitions_cache(name)

def set_values(self, name, values, rules):
pos = valuesname(values)
definition = self.get_user_definition(self.lookup_name(name))
definition.set_values_list(pos, rules)
self.clear_definitions_cache(name)

def get_options(self, name):
return self.get_definition(self.lookup_name(name)).options

def reset_user_definitions(self):
self.user = {}
self.clear_cache()

def get_user_definitions(self):
if six.PY2:
Expand All @@ -397,6 +474,7 @@ def set_user_definitions(self, definitions):
self.user = pickle.loads(base64.decodebytes(definitions.encode('ascii')))
else:
self.user = {}
self.clear_cache()

def get_ownvalue(self, name):
ownvalues = self.get_definition(self.lookup_name(name)).ownvalues
Expand All @@ -410,14 +488,18 @@ def set_ownvalue(self, name, value):

name = self.lookup_name(name)
self.add_rule(name, Rule(Symbol(name), value))
self.clear_cache(name)

def set_options(self, name, options):
definition = self.get_user_definition(self.lookup_name(name))
definition.options = options
self.clear_definitions_cache(name)

def unset(self, name, expr):
definition = self.get_user_definition(self.lookup_name(name))
return definition.remove_rule(expr)
result = definition.remove_rule(expr)
self.clear_definitions_cache(name)
return result

def get_config_value(self, name, default=None):
'Infinity -> None, otherwise returns integer.'
Expand Down