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

Refactor resolver into a tree of callable objects, or partially evaluated #95

Merged
merged 19 commits into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
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
40 changes: 31 additions & 9 deletions fluent.runtime/fluent/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
from fluent.syntax.ast import Message, Term

from .builtins import BUILTINS
from .resolver import resolve
from .utils import ATTRIBUTE_SEPARATOR, TERM_SIGIL, add_message_and_attrs_to_store, ast_to_id
from .prepare import Compiler
from .resolver import ResolverEnvironment, CurrentEnvironment
from .utils import ATTRIBUTE_SEPARATOR, TERM_SIGIL, ast_to_id, native_to_fluent


class FluentBundle(object):
Expand All @@ -33,6 +34,8 @@ def __init__(self, locales, functions=None, use_isolating=True):
self._functions = _functions
self._use_isolating = use_isolating
self._messages_and_terms = {}
self._compiled = {}
self._compiler = Compiler(use_isolating=use_isolating)
self._babel_locale = self._get_babel_locale()
self._plural_form = babel.plural.to_python(self._babel_locale.plural_form)

Expand All @@ -44,22 +47,41 @@ def add_messages(self, source):
if isinstance(item, (Message, Term)):
full_id = ast_to_id(item)
if full_id not in self._messages_and_terms:
# We add attributes to the store to enable faster looker
# later, and more direct code in some instances.
add_message_and_attrs_to_store(self._messages_and_terms, full_id, item)
self._messages_and_terms[full_id] = item

def has_message(self, message_id):
if message_id.startswith(TERM_SIGIL) or ATTRIBUTE_SEPARATOR in message_id:
return False
return message_id in self._messages_and_terms

def lookup(self, full_id):
if full_id not in self._compiled:
entry_id = full_id.split(ATTRIBUTE_SEPARATOR, 1)[0]
entry = self._messages_and_terms[entry_id]
compiled = self._compiler(entry)
if compiled.value is not None:
self._compiled[entry_id] = compiled.value
for attr in compiled.attributes:
self._compiled[ATTRIBUTE_SEPARATOR.join([entry_id, attr.id.name])] = attr.value
return self._compiled[full_id]

def format(self, message_id, args=None):
if message_id.startswith(TERM_SIGIL):
raise LookupError(message_id)
message = self._messages_and_terms[message_id]
if args is None:
args = {}
return resolve(self, message, args)
if args is not None:
fluent_args = {
argname: native_to_fluent(argvalue)
for argname, argvalue in args.items()
}
else:
fluent_args = {}

errors = []
resolve = self.lookup(message_id)
env = ResolverEnvironment(context=self,
current=CurrentEnvironment(args=fluent_args),
errors=errors)
return [resolve(env), errors]

def _get_babel_locale(self):
for l in self.locales:
Expand Down
53 changes: 53 additions & 0 deletions fluent.runtime/fluent/runtime/prepare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import absolute_import, unicode_literals
from fluent.syntax import ast as FTL
from . import resolver


class Compiler(object):
def __init__(self, use_isolating=False):
self.use_isolating = use_isolating

def __call__(self, item):
if isinstance(item, FTL.BaseNode):
return self.compile(item)
if isinstance(item, (tuple, list)):
return [self(elem) for elem in item]
return item

def compile(self, node):
nodename = type(node).__name__
if not hasattr(resolver, nodename):
return node
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this kind of dynamic lookup of attributes on the resolver module, I think it would help comprehension if:

  1. The resolver module gained an __all__ attribute for show what it is exporting clearly, including all the BaseResolver subclasses.
  2. Somewhere next to that there was a brief explanation of how these classes are looked up.

Otherwise someone new to the code base sees the resolver.Message class, for instance, but can't find any other references to it or work out how it is actually used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a doc string to the module and cleaned up the global namespace a bit more.

I didn't go for adding an __all__ after all. I started doing it, and then looked at the impact it had on our code paths, and there weren't that I could see. In particular, the pre-evaluator imports the module, and the complete namespace is on the module object. The only thing that would differ is from fluent.runtime.resolver import *, which we're not doing. Thus the __all__ would mostly be a source of typos and overlooked changes.

kwargs = vars(node).copy()
for propname, propvalue in kwargs.items():
kwargs[propname] = self(propvalue)
handler = getattr(self, 'compile_' + nodename, self.compile_generic)
return handler(nodename, **kwargs)

def compile_generic(self, nodename, **kwargs):
return getattr(resolver, nodename)(**kwargs)

def compile_Placeable(self, _, expression, **kwargs):
if self.use_isolating:
return resolver.IsolatingPlaceable(expression=expression, **kwargs)
if isinstance(expression, resolver.Literal):
return expression
return resolver.Placeable(expression=expression, **kwargs)

def compile_Pattern(self, _, elements, **kwargs):
if (
len(elements) == 1 and
isinstance(elements[0], resolver.IsolatingPlaceable)
):
# Don't isolate isolated placeables
return elements[0].expression
if any(
not isinstance(child, resolver.Literal)
for child in elements
):
return resolver.Pattern(elements=elements, **kwargs)
if len(elements) == 1:
return elements[0]
return resolver.TextElement(
''.join(child(None) for child in elements)
)
Loading