Commits

Kirill Simonov committed 959e56b

Refactoring error reporting API.

  • Participants
  • Parent commits 9dc3e57

Comments (0)

Files changed (43)

 syntax: glob
 *.pyc
 *.pyo
+*.orig
 .*.sw?
 *.egg-info
 *.env

File src/htsql/core/__init__.py

         wsgi)
 from .validator import DBVal, StrVal, BoolVal
 from .addon import Addon, Parameter, Variable, addon_registry
-from .connect import connect, DBError
+from .connect import connect
+from .error import Error
 from .introspect import introspect
 from .cache import GeneralCache
 
             raise ValueError("database address is not specified")
         try:
             connect().release()
-        except DBError, exc:
+        except Error, exc:
             raise ValueError("failed to establish database connection: %s"
                              % exc)
         try:
             introspect()
-        except DBError, exc:
+        except Error, exc:
             raise ValueError("failed to introspect the database: %s" % exc)
 
 

File src/htsql/core/cmd/act.py

 
 
 from ..adapter import Adapter, adapt
-from ..error import Error, EmptyMark
+from ..error import Error, act_guard
 from ..util import Clonable
 from .command import Command, UniversalCmd, DefaultCmd, FormatCmd, FetchCmd
 from .summon import recognize
 
 
 class UnsupportedActionError(Error):
-
-    def __init__(self, detail):
-        super(UnsupportedActionError, self).__init__(detail, EmptyMark())
+    pass
 
 
 class Action(Clonable):
     assert isinstance(action, Action)
     if not isinstance(command, Command):
         command = recognize(command)
-    return Act.__invoke__(command, action)
+    with act_guard(command.mark):
+        return Act.__invoke__(command, action)
 
 
 def produce(command, environment=None, **parameters):

File src/htsql/core/cmd/summon.py

 
 
 from ..adapter import Adapter, Protocol, adapt, call
-from ..error import Error
+from ..error import Error, recognize_guard
 from ..util import to_name
 from ..syn.syntax import (Syntax, SkipSyntax, FunctionSyntax, PipeSyntax,
         ApplySyntax, CollectSyntax)
     adapt(FunctionSyntax)
 
     def __call__(self):
-        return Summon.__invoke__(self.syntax)
+        with recognize_guard(self.syntax):
+            return Summon.__invoke__(self.syntax)
 
 
 class RecognizePipe(Recognize):
     def __call__(self):
         if self.syntax.is_flow:
             return super(RecognizePipe, self).__call__()
-        return Summon.__invoke__(self.syntax)
+        with recognize_guard(self.syntax):
+            return Summon.__invoke__(self.syntax)
 
 
 class RecognizeCollect(Recognize):
 
 class SummonFetch(Summon):
 
-    call('fetch', 'retrieve')
+    call('fetch',
+         'retrieve')
 
     def __call__(self):
         if len(self.arguments) != 1:
-            raise Error("expected 1 argument", self.syntax.mark)
+            raise Error("Expected 1 argument")
         [syntax] = self.arguments
         return FetchCmd(syntax, self.syntax.mark)
 
 
     def __call__(self):
         if len(self.arguments) != 1:
-            raise Error("expected 1 argument", self.syntax.mark)
+            raise Error("Expected 1 argument")
         [syntax] = self.arguments
         feed = recognize(syntax)
         format = self.format()
 
     def __call__(self):
         if len(self.arguments) != 1:
-            raise Error("expected 1 argument", self.syntax.mark)
+            raise Error("Expected 1 argument")
         [syntax] = self.arguments
         feed = recognize(syntax)
         return SQLCmd(feed, self.syntax.mark)

File src/htsql/core/connect.py

 
 from .adapter import Adapter, Utility, adapt
 from .domain import Domain, Record
-from .error import EngineError
+from .error import Error, EngineError, QuotePara
 from .context import context
 
 
-class DBError(Exception):
-    """
-    Raised when a database error occurred.
-
-    `message` (a string)
-        The error message.
-    """
-
-    def __init__(self, message):
-        assert isinstance(message, str)
-        self.message = message
-
-    def __str__(self):
-        return self.message
-
-    def __repr__(self):
-        return "<%s %s>" % (self.__class__.__name__, self)
-
-
-class ErrorGuard(object):
+class DBErrorGuard(object):
     """
     Guards against DBAPI exception.
 
-    This class converts DBAPI exceptions to :exc:`DBError`.  It is designed
+    This class converts DBAPI exceptions to :exc:`EngineError`.  It is designed
     to be used in a ``with`` clause.
 
     Usage::
 
         connect = Connect()
         try:
-            with ErrorGuard():
+            with DBErrorGuard():
                 connection = connect.open_connection()
                 cursor = connection.cursor()
                 ...
-        except DBError:
+        except EngineError:
             ...
     """
 
 
         # If we got a new exception, raise it.
         if error is not None:
-            raise error
+            raise EngineError(QuotePara("Got an error from"
+                                        " the database driver", error))
 
 
 class ConnectionProxy(object):
 
     The proxy supports common DBAPI methods by passing them to
     the connection object.  Any exceptions raised when the executing
-    DBAPI methods are converted to :exc:`DBError`.
+    DBAPI methods are converted to :exc:`Error`.
 
     `connection`
         A raw DBAPI connection object.
 
-    `guard` (:class:`ErrorGuard`)
+    `guard` (:class:`DBErrorGuard`)
         A DBAPI exception guard.
     """
 
 
     The proxy supports common DBAPI methods by passing them to
     the cursor object.  Any exceptions raised when the executing
-    DBAPI methods are converted to :exc:`DBError`.
+    DBAPI methods are converted to :exc:`Error`.
 
     `cursor`
         A raw DBAPI cursor object.
 
-    `guard` (:class:`ErrorGuard`)
+    `guard` (:class:`DBErrorGuard`)
         A DBAPI exception guard.
     """
 
         """
         Execute one SQL statement.
         """
-        with self.guard:
-            return self.cursor.execute(statement, *parameters)
+        addon = context.app.htsql
+        if addon.debug:
+            try:
+                with self.guard:
+                    return self.cursor.execute(statement, *parameters)
+            except Error, exc:
+                exc.wrap(QuotePara("While executing SQL", statement))
+                if parameters:
+                    parameters = parameters[0]
+                    exc.wrap(QuotePara("With parameters",
+                                       repr(list(parameters))))
+                raise
+        else:
+            with self.guard:
+                return self.cursor.execute(statement, *parameters)
 
-    def executemany(self, statement, *parameters):
+    def executemany(self, statement, parameters_set):
         """
         Execute an SQL statement against all parameters.
         """
-        with self.guard:
-            return self.cursor.executemany(statement, *parameters)
+        addon = context.app.htsql
+        if addon.debug:
+            try:
+                with self.guard:
+                    return self.cursor.executemany(statement, parameters_set)
+            except Error, exc:
+                exc.wrap(QuotePara("While executing SQL", statement))
+                if not parameters_set:
+                    exc.wrap("With no parameters")
+                else:
+                    exc.wrap(QuotePara("With a set of parameters",
+                                       "\n".join(repr(list(group))
+                                                 for group in parameters_set)))
+                raise
+        else:
+            with self.guard:
+                return self.cursor.executemany(statement, parameters_set)
 
     def fetchone(self):
         """
             cursor.execute(...)
             cursor.fetchall()
             ...
-        except DBError:
+        except Error:
             ...
     """
 
         and supports common DBAPI methods.
 
         If the database parameters for the application are invalid,
-        the method may raise :exc:`DBError`.
+        the method may raise :exc:`Error`.
         """
         # Create a guard for DBAPI exceptions.
-        guard = ErrorGuard()
+        guard = DBErrorGuard()
         # Open a raw connection while intercepting DBAPI exceptions.
         with guard:
             connection = self.open()
 
     def __enter__(self):
         if self.connection is None:
-            try:
-                connection = connect()
-            except DBError, exc:
-                raise EngineError("failed to open a database connection: %s"
-                                  % exc)
+            connection = connect()
             context.env.push(connection=connection)
             return connection
         return self.connection
                 #connection.rollback()
                 connection.invalidate()
             connection.release()
-            if exc_type is not None and issubclass(exc_type, DBError):
-                if isinstance(exc_value, Exception):
-                    exception = exc_value
-                elif exc_value is None:
-                    exception = exc_type()
-                elif isinstance(exc_value, tuple):
-                    exception = exc_type(*exc_value)
-                else:
-                    exception = exc_type(exc_value)
-                raise EngineError("failed to execute a database query: %s"
-                                  % exception)
 
 
 def transaction():

File src/htsql/core/domain.py

         text = ContainerDomain.dump_entry(self.data, self.domain)
         # Make sure the output is printable.
         text = urlquote(text, "")
-        return u"%s: %s" % (text, self.meta)
+        return text
 
     def __iter__(self):
         if not (isinstance(self.domain, ListDomain) and self.data is not None):

File src/htsql/core/error.py

 #
 
 
-from .util import Printable, maybe
+from .util import Printable, maybe, listof, oneof
 
 
 #
     status = None
 
     def __call__(self, environ, start_response):
-        # Implement the WSGI entry point.
+        # Implement a WSGI entry point.
         start_response(self.status,
                        [('Content-Type', 'text/plain; charset=UTF-8')])
         return [str(self), "\n"]
 #
 
 
+class Paragraph(Printable):
+
+    def __init__(self, message):
+        assert isinstance(message, (str, unicode))
+        if isinstance(message, str):
+            message = message.decode('utf-8', 'replace')
+        self.message = message
+
+    def __unicode__(self):
+        return self.message
+
+
+class PointerPara(Paragraph):
+
+    def __init__(self, message, mark):
+        super(PointerPara, self).__init__(message)
+        assert isinstance(mark, Mark)
+        self.mark = mark
+
+    def __unicode__(self):
+        if not self.mark:
+            return self.message
+        lines = self.mark.excerpt()
+        pointer = u"\n".join(u"    "+line for line in lines)
+        return u"%s:\n%s" % (self.message, pointer)
+
+
+class QuotePara(Paragraph):
+
+    def __init__(self, message, quote):
+        assert isinstance(quote, (str, unicode))
+        if isinstance(quote, str):
+            quote = quote.decode('utf-8', 'replace')
+        super(QuotePara, self).__init__(message)
+        self.quote = quote.rstrip()
+
+    def __unicode__(self):
+        if not self.quote:
+            return self.message
+        block = "\n".join(u"    "+line for line in self.quote.splitlines())
+        return u"%s:\n%s" % (self.message, block)
+
+
+class ChoicePara(Paragraph):
+
+    def __init__(self, message, choices):
+        super(PointerPara, self).__init__(message)
+        assert isinstance(choices, listof(oneof(str, unicode)))
+        choices = [choice.decode('utf-8', 'replace')
+                   if isinstance(choice, str) else choice]
+        self.choices = choices
+
+    def __unicode__(self):
+        if not self.choices:
+            return self.message
+        return u"%s:\n%s" % (self.message,
+                             u"\n".join(u"    "+choice for choice in choices))
+
+
 class Mark(Printable):
     """
     A fragment of an HTSQL query.
         Returns a list of lines that forms an excerpt of the original query
         string with ``^`` characters underlining the marked fragment.
         """
+        if not self.text:
+            return u""
         # Find the line that contains the mark.
         excerpt_start = self.text.rfind(u'\n', 0, self.start)+1
         excerpt_end = self.text.find(u'\n', excerpt_start)
         return lines
 
     def __unicode__(self):
-        return u">>> %s <<<" % self.text[self.start:self.end]
+        return u"\n".join(self.excerpt())
+
+    def __nonzero__(self):
+        return bool(self.text)
 
 
 class EmptyMark(Mark):
         super(EmptyMark, self).__init__(u"", 0, 0)
 
 
-class StackedError(Exception):
+class Error(BadRequestError):
+
+    def __init__(self, *paragraphs):
+        self.paragraphs = [paragraph if isinstance(paragraph, Paragraph)
+                           else Paragraph(paragraph)
+                           for paragraph in paragraphs]
+
+    def wrap(self, *paragraphs):
+        self.paragraphs.extend(paragraph if isinstance(paragraph, Paragraph)
+                               else Paragraph(paragraph)
+                               for paragraph in paragraphs)
+
+    def __unicode__(self):
+        return u"\n".join(unicode(paragraph) for paragraph in self.paragraphs)
+
+    def __str__(self):
+        return unicode(self).encode('utf-8')
+
+    def __repr__(self):
+        if not self.paragraphs:
+            return "<%s>" % self.__class__.__name__
+        return "<%s: %s>" % (self.__class__.__name__,
+                             self.paragraphs[0].message.encode('utf-8'))
+
+
+class EngineError(ConflictError, Error):
     """
-    An exception with a query fragment as the error context.
-
-    `detail`: ``str``
-        The error message.
-
-    `mark`: :class:`Mark`
-        A pointer to a query fragment.
-
-    `hint`: ``str``
-        Explanation of the error and possible ways to fix it.
+    An error generated by the database driver.
     """
 
-    def __init__(self, detail, mark, hint=None):
-        assert isinstance(mark, Mark)
-        assert isinstance(hint, maybe(str))
-        super(StackedError, self).__init__(detail)
-        self.detail = detail
-        self.mark = mark
-        self.hint = hint
 
-    def __str__(self):
-        if not self.mark.text:
-            return self.detail
-        lines = self.mark.excerpt()
-        mark_detail = "\n".join("    "+line.encode('utf-8') for line in lines)
-        return "%s:\n%s%s" % (self.detail, mark_detail,
-                              "\n(%s)" % self.hint
-                                            if self.hint is not None else "")
-
-
-class Error(StackedError, BadRequestError):
-    """
-    An error caused by user mistake.
-    """
-
-
-class EngineError(StackedError, ConflictError):
-    """
-    An error generated by the database server.
-    """
-
-    def __init__(self, detail):
-        super(EngineError, self).__init__(detail, EmptyMark())
-
-
-class PermissionError(StackedError, ForbiddenError):
+class PermissionError(ForbiddenError, Error):
     """
     An error caused by lack of read or write permissions.
     """
 
-    def __init__(self, detail):
-        super(PermissionError, self).__init__(detail, EmptyMark())
 
+class ErrorGuard(object):
 
+    def __init__(self, *paragraphs):
+        self.paragraphs = paragraphs
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        if isinstance(exc_value, Error):
+            exc_value.wrap(*self.paragraphs)
+
+
+class PointerErrorGuard(object):
+
+    def __init__(self, message, mark):
+        self.message = message
+        if mark is not None:
+            if not isinstance(mark, Mark):
+                mark = mark.mark
+        self.mark = mark
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        if not isinstance(exc_value, Error):
+            return
+        if not self.mark:
+            return
+        if any(paragraph.mark.text == self.mark.text
+               for paragraph in exc_value.paragraphs
+               if isinstance(paragraph, PointerPara)):
+            return
+        exc_value.wrap(PointerPara(self.message, self.mark))
+
+
+def parse_guard(mark):
+    return PointerErrorGuard("While parsing", mark)
+
+
+recognize_guard = parse_guard
+
+
+def translate_guard(mark):
+    return PointerErrorGuard("While translating", mark)
+
+
+def act_guard(mark):
+    return PointerErrorGuard("While processing", mark)
+
+
+def choices_guard(choices):
+    if not choices:
+        return ErrorGuard()
+    quote = "\n".join(choices)
+    return ErrorGuard(QuotePara("Perhaps you had in mind", quote))
+
+

File src/htsql/core/fmt/html.py

 from ..adapter import Adapter, adapt, adapt_many
 from .format import HTMLFormat
 from .emit import EmitHeaders, Emit
-from ..error import StackedError, InternalServerError, Mark
+from ..error import Error, InternalServerError, PointerPara, Mark
 from ..domain import (Domain, BooleanDomain, NumberDomain, DecimalDomain,
         TextDomain, EnumDomain, DateDomain, TimeDomain, DateTimeDomain,
         ListDomain, RecordDomain, UntypedDomain, VoidDomain, OpaqueDomain,
         return "".join(str(case) for case in self.cases)
 
 
-class TemplateError(StackedError, InternalServerError):
-    pass
+class TemplateError(InternalServerError, Error):
+
+    def __init__(self, message, mark):
+        super(TemplateError, self).__init__(PointerPara(message, mark))
 
 
 class Template(object):

File src/htsql/core/syn/decode.py

 #
 
 
-from ..error import Error, Mark
+from ..error import Error, Mark, parse_guard
 import re
 import urllib2
 
         start = len(match.string[:start].decode('utf-8', 'ignore'))
         end = len(match.string[:end].decode('utf-8', 'ignore'))
         mark = Mark(text, start, end)
-        raise Error("symbol '%' must be followed by two hexdecimal"
-                    " digits", mark)
+        with parse_guard(mark):
+            raise Error("Expected symbol `%` followed by two hexdecimal digits")
     # Return the character corresponding to the escape sequence.
     return chr(int(code, 16))
 
         start = len(text[:exc.start].decode('utf-8', 'ignore'))
         end = len(text[:exc.end].decode('utf-8', 'ignore'))
         mark = Mark(text.decode('utf-8', 'replace'), start, end)
-        raise Error("cannot convert a byte sequence %s to UTF-8: %s"
-                    % (urllib2.quote(exc.object[exc.start:exc.end]),
-                       exc.reason), mark)
+        with parse_guard(mark):
+            raise Error("Cannot convert a byte sequence %s to UTF-8: %s"
+                        % (urllib2.quote(exc.object[exc.start:exc.end]),
+                           exc.reason))
 
     return text
 

File src/htsql/core/syn/grammar.py

 
 from ..util import (maybe, oneof, listof, omapof, trim_doc, toposort, omap,
         TextBuffer, Printable)
-from ..error import Error, Mark
+from ..error import Error, Mark, parse_guard
 from .token import Token
 import re
 
             # Complain if no token is found.
             if match is None:
                 mark = Mark(text, position, position)
-                if position < len(text):
-                    raise Error("unexpected character %r"
-                                % text[position].encode('utf-8'), mark)
-                else:
-                    raise Error("unexpected end", mark)
+                with parse_guard(mark):
+                    if position < len(text):
+                        raise Error("Got unexpected character %r"
+                                    % text[position].encode('utf-8'))
+                    else:
+                        raise Error("Got unexpected end of input")
             # The position of the next token.
             next_position = match.end()
             # The error context for the new token.
                 assert False
             # Report an error for an error rule.
             if group.error is not None:
-                raise Error(group.error, mark)
+                with parse_guard(mark):
+                    raise Error(group.error)
             # Generate a new token object.
             if not group.is_junk:
                 if group.unquote:
                     error = table.fail(stream, token)
                     if error is not None:
                         raise error
-                if token:
-                    raise Error("unexpected input", token.mark)
-                else:
-                    raise Error("unexpected end of input", token.mark)
+                with parse_guard(token):
+                    if token:
+                        raise Error("Got unexpected input")
+                    else:
+                        raise Error("Got unexpected end of input")
 
             # The non-terminal associated with the transition and the next
             # machine state.
                 # Complain if we are exiting from the top rule
                 # and there are still tokens left.
                 if not stack and token:
-                    if table.fail is not None:
-                        error = table.fail(stream, token)
-                        if error is not None:
-                            raise error
-                    raise Error("unexpected input", token.mark)
+                    with parse_guard(token):
+                        if table.fail is not None:
+                            error = table.fail(stream, token)
+                            if error is not None:
+                                raise error
+                        raise Error("Got unexpected input")
                 # Process accumulated nodes.
                 if table.match is not None:
                     production = list(table.match(stream))

File src/htsql/core/tr/bind.py

         FloatDomain, UntypedDomain, EntityDomain, RecordDomain, ListDomain,
         IdentityDomain, VoidDomain)
 from ..classify import normalize
-from ..error import Error
+from ..error import Error, QuotePara, translate_guard, choices_guard
 from ..syn.syntax import (Syntax, CollectSyntax, SelectSyntax, ApplySyntax,
         FunctionSyntax, PipeSyntax, OperatorSyntax, PrefixSyntax,
         ProjectSyntax, FilterSyntax, LinkSyntax, DetachSyntax, AssignSyntax,
             If set, the lookup scope is set to `scope` when
             binding the syntax node.
         """
-        return bind(syntax, self, scope)
+        with translate_guard(syntax):
+            return bind(syntax, self, scope)
 
     def use(self, recipe, syntax, scope=None):
         """
         if scope is not None:
             self.push_scope(scope)
         # Realize and apply `BindByRecipe` adapter.
-        binding = BindByRecipe.__invoke__(recipe, syntax, self)
+        with translate_guard(syntax):
+            binding = BindByRecipe.__invoke__(recipe, syntax, self)
         # Restore the old lookup scope.
         if scope is not None:
             self.pop_scope()
         if scope is not None:
             self.push_scope(scope)
         # Realize and apply `BindByName` protocol.
-        binding = BindByName.__invoke__(syntax, self)
+        with translate_guard(syntax):
+            binding = BindByName.__invoke__(syntax, self)
         # Restore the old lookup scope.
         if scope is not None:
             self.pop_scope()
     def __call__(self):
         # The default implementation raises an error.  It is actually
         # unreachable since we provide an implementation for all syntax nodes.
-        raise Error("unable to bind a node", self.syntax.mark)
+        raise Error("Unable to bind a node")
 
 
 def hint_choices(choices):
         if self.syntax.arm is not None:
             seed = self.state.bind(self.syntax.arm)
             if isinstance(seed, AssignmentBinding):
-                if len(seed.terms) != 1:
-                    raise Error("qualified definition is not allowed"
-                                " for an in-segment assignment",
-                                seed.mark)
-                if seed.parameters is not None:
-                    raise Error("parameterized definition is not allowed"
-                                " for an in-segment assignment",
-                                seed.mark)
+                with translate_guard(seed):
+                    if len(seed.terms) != 1:
+                        raise Error("Qualified definition is not allowed"
+                                    " for an in-segment assignment")
+                    if seed.parameters is not None:
+                        raise Error("Parameterized definition is not allowed"
+                                    " for an in-segment assignment")
                 name, is_reference = seed.terms[0]
                 if is_reference:
                     recipe = BindingRecipe(self.state.bind(seed.body))
         if domain is None:
             # FIXME: separate implementation for VoidDomain with a better error
             # message.
-            raise Error("output column must be scalar",
-                        self.binding.mark)
+            raise Error("Output column must be scalar")
         return ImplicitCastBinding(self.binding, domain, self.binding.syntax)
 
 
             binding = self.state.bind(arm)
             # Handle in-selector assignments.
             if isinstance(binding, AssignmentBinding):
-                if len(binding.terms) != 1:
-                    raise Error("qualified definition is not allowed"
-                                " for an in-selector assignment",
-                                binding.mark)
-                if binding.parameters is not None:
-                    raise Error("parameterized definition is not allowed"
-                                " for an in-selector assignment",
-                                binding.mark)
+                with translate_guard(binding):
+                    if len(binding.terms) != 1:
+                        raise Error("Qualified definition is not allowed"
+                                    " for an in-selector assignment")
+                    if binding.parameters is not None:
+                        raise Error("Parameterized definition is not allowed"
+                                    " for an in-selector assignment")
                 name, is_reference = binding.terms[0]
                 if is_reference:
                     recipe = BindingRecipe(self.state.bind(binding.body))
         kernels = []
         for element in elements:
             domain = coerce(element.domain)
-            if domain is None:
-                raise Error("quotient column must be scalar",
-                            element.mark)
+            with translate_guard(element):
+                if domain is None:
+                    raise Error("Expected a scalar column")
             kernel = ImplicitCastBinding(element, domain, element.syntax)
             kernels.append(kernel)
         # Generate the quotient scope.
             target_images.append(binding)
         # Correlate origin and target images.
         if len(origin_images) != len(target_images):
-            raise Error("unbalanced origin and target columns",
-                        self.syntax.mark)
+            raise Error("Found unbalanced origin and target columns")
         images = []
         for origin_image, target_image in zip(origin_images, target_images):
             domain = coerce(origin_image.domain, target_image.domain)
             if domain is None:
-                raise Error("cannot coerce origin and target columns"
-                            " to a common type", self.syntax.mark)
+                raise Error("Cannot coerce origin and target columns"
+                            " to a common type")
             origin_image = ImplicitCastBinding(origin_image, domain,
                                                origin_image.syntax)
             target_image = ImplicitCastBinding(target_image, domain,
         syntax = self.syntax.larm
         for idx, arm in enumerate(syntax.larms):
             if isinstance(arm, ReferenceSyntax):
-                if idx < len(syntax.larms)-1:
-                    raise Error("an identifier is expected",
-                                arm.mark)
+                with translate_guard(arm):
+                    if idx < len(syntax.larms)-1:
+                        raise Error("Expected an identifier")
                 terms.append((arm.identifier.name, True))
             else:
                 terms.append((arm.name, False))
     def __call__(self):
         seed = self.state.bind(self.syntax.larm)
         recipe = identify(seed)
-        if recipe is None:
-            raise Error("cannot determine identity", seed.mark)
+        with translate_guard(seed):
+            if recipe is None:
+                raise Error("Cannot determine identity")
         identity = self.state.use(recipe, self.syntax.rarm, scope=seed)
         location = self.state.bind(self.syntax.rarm, scope=seed)
-        if identity.domain.width != location.width:
-            raise Error("ill-formed locator", self.syntax.rarm.mark)
+        with translate_guard(self.syntax.rarm):
+            if identity.domain.width != location.width:
+                raise Error("Found ill-formed locator")
         def convert(identity, elements):
             value = []
             for field in identity.labels:
                         else:
                             items.append(element)
                             total_width += 1
-                    if total_width > field.width:
-                        raise Error("ill-formed locator",
-                                        self.syntax.rarm.mark)
+                    with translate_guard(self.syntax.rarm):
+                        if total_width > field.width:
+                            raise Error("Found ill-formed locator")
                     item = convert(field, items)
                     value.append(item)
                 else:
                     assert elements
                     element = elements.pop(0)
-                    if isinstance(element, IdentityBinding):
-                        raise Error("ill-formed locator",
-                                        self.syntax.larm.mark)
+                    with translate_guard(self.syntax.larm):
+                        if isinstance(element, IdentityBinding):
+                            raise Error("Found ill-formed locator")
                     item = ImplicitCastBinding(element, field, element.syntax)
                     value.append(item)
             return tuple(value)
         recipes = expand(self.state.scope, with_syntax=True, with_wild=True,
                          with_class=True, with_link=True)
         if recipes is None:
-            raise Error("cannot expand '*' since output columns"
-                        " are not defined", self.syntax.mark)
+            raise Error("Cannot expand '*' since output columns"
+                        " are not defined")
         # If a position is given, extract a specific element.
         if self.syntax.index is not None:
             index = self.syntax.index
             index -= 1
             if not (0 <= index < len(recipes)):
-                raise Error("value in range 1-%s is required"
-                            % len(recipes), self.syntax.mark)
+                raise Error("Expected value in range 1-%s" % len(recipes))
             syntax, recipe = recipes[index]
             syntax = syntax.clone(mark=self.syntax.mark)
             return self.state.use(recipe, syntax)
             names = lookup_reference_set(self.state.scope)
             choices = [u"$"+name for name in sorted(names)
                                  if similar(model, name)]
-            hint = hint_choices(choices)
-            raise Error("unrecognized reference '%s'"
-                        % self.syntax, self.syntax.mark,
-                        hint=hint)
+            with choices_guard(choices):
+                raise Error(QuotePara("Found unknown reference",
+                                      str(self.syntax)))
         return self.state.use(recipe, self.syntax)
 
 
         # Look for a complement, complain if not found.
         recipe = lookup_complement(self.state.scope)
         if recipe is None:
-            raise Error("'^' could only be used in a quotient scope",
-                        self.syntax.mark)
+            raise Error("'^' could only be used in a quotient scope")
         return self.state.use(recipe, self.syntax)
 
 
 
     def __call__(self):
         # The default implementation; override in subclasses.
-        hint = None
         # Generate a hint with a list of alternative names.
         model = self.name.lower()
         arity = None
             component_name = component_name.lower()
             global_attributes.add((component_name, component_arity))
         all_attributes = sorted(attributes|global_attributes)
-        if hint is None and arity is None:
+        choices = []
+        if not choices and arity is None:
             names = lookup_reference_set(self.state.scope)
             if model in names:
-                hint = "did you mean: a reference '$%s'" % model.encode('utf-8')
-        if hint is None and arity is None:
+                choices = ["a reference '$%s'" % model.encode('utf-8')]
+        if not choices and arity is None:
             if any(model == sample
                    for sample, sample_arity in all_attributes
                    if sample_arity is not None):
-                hint = "did you mean: a function '%s'" % model.encode('utf-8')
-        if hint is None and arity is None:
+                choices = ["a function '%s'" % model.encode('utf-8')]
+        if not choices and arity is None:
             choices = [sample
                        for sample, sample_arity in all_attributes
                        if sample_arity is None and sample != model
                             and similar(model, sample)]
-            hint = hint_choices(choices)
-        if hint is None and arity is not None \
+        if not choices and arity is not None \
                 and not isinstance(self.syntax, OperatorSyntax):
             arities = [sample_arity
                        for sample, sample_arity in all_attributes
                 else:
                     required_arity.append("arguments")
                 required_arity = " ".join(required_arity)
-                raise Error("function '%s' requires %s; got %s"
+                raise Error("Function '%s' requires %s; got %s"
                             % (self.syntax.identifier,
-                               required_arity, arity),
-                            self.syntax.mark)
-        if hint is None and arity is not None:
+                               required_arity, arity))
+        if not choices and arity is not None:
             if any(model == sample
                    for sample, sample_arity in all_attributes
                    if sample_arity is None):
-                hint = "did you mean: an attribute '%s'" % model.encode('utf-8')
-        if hint is None and arity is not None:
+                choices = ["an attribute '%s'" % model.encode('utf-8')]
+        if not choices and arity is not None:
             choices = [sample
                        for sample, sample_arity in all_attributes
                        if sample_arity in [-1, arity] and sample != model
                             and similar(model, sample)]
-            hint = hint_choices(choices)
         scope_name = guess_tag(self.state.scope)
         if scope_name is not None:
             scope_name = scope_name.encode('utf-8')
-        if isinstance(self.syntax, (FunctionSyntax, PipeSyntax)):
-            raise Error("unrecognized function '%s'"
-                        % self.syntax.identifier,
-                        self.syntax.mark, hint=hint)
-        if isinstance(self.syntax, OperatorSyntax):
-            raise Error("unrecognized operator '%s'"
-                        % self.syntax.symbol.encode('utf-8'),
-                        self.syntax.mark, hint=hint)
-        if isinstance(self.syntax, PrefixSyntax):
-            raise Error("unrecognized unary operator '%s'"
-                        % self.syntax.symbol.encode('utf-8'),
-                        self.syntax.mark, hint=hint)
-        if isinstance(self.syntax, IdentifierSyntax):
-            raise Error("unrecognized attribute '%s'%s"
-                        % (self.syntax,
-                           " in scope of '%s'" % scope_name
-                           if scope_name is not None else ""),
-                        self.syntax.mark, hint=hint)
+        with choices_guard(choices):
+            if isinstance(self.syntax, (FunctionSyntax, PipeSyntax)):
+                raise Error(QuotePara("Found unknown function",
+                                      str(self.syntax.identifier)))
+            if isinstance(self.syntax, OperatorSyntax):
+                raise Error(QuotePara("Found unknown operator",
+                                      self.syntax.symbol.encode('utf-8')))
+            if isinstance(self.syntax, PrefixSyntax):
+                raise Error(QuotePara("Found unknown unary operator",
+                                      self.syntax.symbol.encode('utf-8')))
+            if isinstance(self.syntax, IdentifierSyntax):
+                raise Error(QuotePara("Found unknown attribute",
+                                      "%s.%s" % (scope_name, self.syntax)
+                                      if scope_name is not None
+                                      else str(self.syntax)))
 
 
 class BindByRecipe(Adapter):
 
     def __call__(self):
         # The default implementation should not be reachable.
-        raise Error("unable to bind a node", self.syntax.mark)
+        raise Error("unable to bind a node")
 
 
 class BindByLiteral(BindByRecipe):
                 arity = len(self.recipe.parameters)
             recipe = lookup_attribute(self.recipe.base, self.syntax.name)
             if recipe is None:
-                raise Error("unrecognized attribute '%s'" % self.syntax,
-                            self.syntax.mark)
+                raise Error(QuotePara("Found unknown attribute",
+                                      str(self.syntax)))
             binding = self.state.use(recipe, self.syntax)
             # Check if the term is a reference.
             if is_reference:
         syntax = self.syntax
         if isinstance(self.syntax, (FunctionSyntax, PipeSyntax)):
             syntax = self.syntax.identifier
-        hint = None
+        int = None
+        choices = []
         if self.recipe.alternatives:
-            alternatives = self.recipe.alternatives
-            choices = ["try "]
-            if len(alternatives) == 1:
-                choices.append(repr(alternatives[0].encode('utf-8')))
-            else:
-                choices.extend(", ".join(repr(alternative.encode('utf-8'))
-                                         for alternative in alternatives[:-1]))
-                choices.append(" or ")
-                choices.append(repr(alternatives[-1].encode('utf-8')))
-            hint = "".join(choices)
-        raise Error("ambiguous name '%s'" % syntax,
-                    self.syntax.mark, hint=hint)
+            choices = [str(alternative)
+                       for alternative in self.recipe.alternatives]
+        with choices_guard(choices):
+            raise Error(QuotePara("Found ambiguous name", str(syntax)))
 
 
 def bind(syntax, state=None, scope=None, environment=None):
         state.set_root(root)
         if isinstance(syntax, AssignSyntax):
             specifier = syntax.larm
-            if not (len(specifier.larms) == 1 and
-                    isinstance(specifier.larms[0], IdentifierSyntax) and
-                    specifier.rarms is None):
-                raise Error("expected an identifier", specifier.mark)
+            with translate_guard(specifier):
+                if specifier.identifier is None:
+                    raise Error("Expected an identifier")
             identifier = specifier.larms[0]
             segment = state.bind(syntax.rarm)
             if not isinstance(segment, SegmentBinding):

File src/htsql/core/tr/compile.py

 from ..util import maybe, listof
 from ..adapter import Adapter, adapt, adapt_many
 from ..domain import BooleanDomain, IntegerDomain
-from ..error import Error
+from ..error import Error, translate_guard
 from .coerce import coerce
 from .signature import (IsNullSig, IsEqualSig, AndSig, CompareSig,
                         SortDirectionSig, RowNumberSig)
         # and mask flows.  Second, each compiled term must have a unique
         # tag, therefore we'd have to replace the tags and route tables
         # of the cached term node.
-        return compile(expression, self, baseline=baseline)
+        with translate_guard(expression):
+            return compile(expression, self, baseline=baseline)
 
     def inject(self, term, expressions):
         """
             if expression in term.routes:
                 continue
             # Inject the expression into the term.
-            term = Inject.__invoke__(expression, term, self)
+            with translate_guard(expression):
+                term = Inject.__invoke__(expression, term, self)
         # Return the augmented term node.
         return term
 
 
     def __call__(self):
         if not self.state.superflow.spans(self.expression.root):
-            raise Error("a singular expression is expected",
-                        self.expression.root.mark)
+            with translate_guard(self.expression.root):
+                raise Error("Expected a singular expression")
         chain = self.state.superflow_stack + \
                 [self.state.superflow, self.expression.root,
                  self.expression.flow]
             return self.term
         # Verify that the unit is singular on the term flow.
         if not self.term.flow.spans(self.flow):
-            raise Error("a singular expression is expected",
-                        self.unit.mark)
+            raise Error("Expected a singular expression")
         # Inject the unit flow into the term.
         return self.state.inject(self.term, [self.unit.flow])
 
 
         # Verify that the unit is singular relative to the term.
         if not self.term.flow.spans(self.flow):
-            raise Error("a singular expression is expected",
-                        self.unit.mark)
+            raise Error("Expected a singular expression")
         # Extract the unit expressions.
         codes = [unit.code for unit in units]
 
 
         # Verify that the units are singular relative to the term.
         if not self.term.flow.spans(self.flow):
-            raise Error("a singular expression is expected",
-                        self.unit.mark)
+            raise Error("Expected a singular expression")
         # Extract the aggregate expressions.
         codes = [unit.code for unit in units]
 
         if not self.term.flow.spans(self.flow):
             # This is not reachable: the error is already reported by
             # the wrapping scalar unit.
-            raise Error("a singular expression is expected",
-                        self.unit.mark)
+            raise Error("Expected a singular expression")
 
         # The general chain of operations is as follows:
         #   - compile a term for the unit flow;
             return self.term
         # Check if the unit is singular against the term flow.
         if not self.term.flow.spans(self.flow):
-            raise Error("a singular expression is expected",
-                        self.unit.mark)
+            raise Error("Expected a singular expression")
         # Inject the quotient space -- this should automatically
         # provide the unit.
         term = self.state.inject(self.term, [self.flow])
         if not self.term.flow.spans(self.flow):
             # Not reachable since covering units are never generated
             # by the user directly, only by the compiler.
-            raise Error("a singular expression is expected",
-                        self.unit.mark)
+            raise Error("Expected a singular expression")
         # FIXME: the rewritter should optimize the flow graph
         # so that this code is not reachable.
         # Add a hint to the flow node to ask the compiler generate

File src/htsql/core/tr/dump.py

 from ..domain import (Domain, BooleanDomain, IntegerDomain, DecimalDomain,
                       FloatDomain, TextDomain, EnumDomain, DateDomain,
                       TimeDomain, DateTimeDomain, ListDomain, RecordDomain)
-from ..error import Error
+from ..error import Error, translate_guard
 from ..syn.syntax import IdentifierSyntax, ApplySyntax, LiteralSyntax
 from .frame import (Clause, Frame, TableFrame, BranchFrame, NestedFrame,
                     SegmentFrame, QueryFrame,
             The clause to serialize.
         """
         # Realize and call the `Serialize` adapter.
-        return serialize(clause, self)
+        with translate_guard(clause):
+            return serialize(clause, self)
 
     def dump(self, clause):
         """
         """
         # Realize and call the `Dump` adapter.
         # Note: returns `None`.
-        return Dump.__invoke__(clause, self)
+        with translate_guard(clause):
+            return Dump.__invoke__(clause, self)
 
     def dub(self, clause):
         """
             The clause to generate an alias for.
         """
         # Realize and call the `Dub` adapter.
-        return Dub.__invoke__(clause, self)
+        with translate_guard(clause):
+            return Dub.__invoke__(clause, self)
 
 
 class Serialize(Adapter):
 
     def __call__(self):
         # By default, generate an error.
-        raise Error("unable to serialize an expression",
-                    self.clause.mark)
+        raise Error("Unable to serialize an expression")
 
     def format(self, template, *namespaces, **keywords):
         """
         # We assume that the database supports 8-byte signed integer values
         # natively and complain if the value is out of this range.
         if not (-2**63 <= self.value < 2**63):
-            raise Error("integer value is out of range",
-                        self.phrase.mark)
+            raise Error("Found integer value is out of range")
         # Write the number; use `(...)` around a negative number.
         if self.value >= 0:
             self.write(unicode(self.value))

File src/htsql/core/tr/encode.py

         BooleanDomain, NumberDomain, IntegerDomain, DecimalDomain, FloatDomain,
         TextDomain, EnumDomain, DateDomain, TimeDomain, DateTimeDomain,
         OpaqueDomain)
-from ..error import Error
+from ..error import Error, translate_guard
 from .coerce import coerce
 from .binding import (Binding, QueryBinding, SegmentBinding,
         WeakSegmentBinding, WrappingBinding, SelectionBinding, HomeBinding,
         # When caching is enabled, we check if `binding` was
         # already encoded.  If not, we encode it and save the
         # result.
-        if self.with_cache:
-            if binding not in self.binding_to_code:
-                #FIXME: reduce recursion depth
-                #code = encode(binding, self)
-                code = Encode.__prepare__(binding, self)()
-                self.binding_to_code[binding] = code
-            return self.binding_to_code[binding]
-        # Caching is disabled; return a new instance every time.
-        return encode(binding, self)
+        with translate_guard(binding):
+            if self.with_cache:
+                if binding not in self.binding_to_code:
+                    #FIXME: reduce recursion depth
+                    #code = encode(binding, self)
+                    code = Encode.__prepare__(binding, self)()
+                    self.binding_to_code[binding] = code
+                return self.binding_to_code[binding]
+            # Caching is disabled; return a new instance every time.
+            return encode(binding, self)
 
     def relate(self, binding):
         """
         # When caching is enabled, we check if `binding` was
         # already encoded.  If not, we encode it and save the
         # result.
-        if self.with_cache:
-            if binding not in self.binding_to_flow:
-                #FIXME: reduce recursion depth
-                #flow = relate(binding, self)
-                flow = Relate.__prepare__(binding, self)()
-                self.binding_to_flow[binding] = flow
-            return self.binding_to_flow[binding]
-        # Caching is disabled; return a new instance every time.
-        return relate(binding, self)
+        with translate_guard(binding):
+            if self.with_cache:
+                if binding not in self.binding_to_flow:
+                    #FIXME: reduce recursion depth
+                    #flow = relate(binding, self)
+                    flow = Relate.__prepare__(binding, self)()
+                    self.binding_to_flow[binding] = flow
+                return self.binding_to_flow[binding]
+            # Caching is disabled; return a new instance every time.
+            return relate(binding, self)
 
 
 class EncodeBase(Adapter):
     def __call__(self):
         # The default implementation generates an error.
         # FIXME: a better error message?
-        raise Error("a code expression is expected",
-                    self.binding.mark)
+        raise Error("Expected a code expression")
 
 
 class Relate(EncodeBase):
     def __call__(self):
         # The default implementation generates an error.
         # FIXME: a better error message?
-        raise Error("a flow expression is expected",
-                    self.binding.mark)
+        raise Error("Expected a flow expression")
 
 
 class EncodeQuery(Encode):
             # More than one dominating flow means the output flow
             # cannot be inferred from the columns unambiguously.
             if len(flows) > 1:
-                raise Error("cannot deduce an unambiguous"
-                            " segment flow",
-                            self.binding.mark)
+                raise Error("Cannot deduce an unambiguous segment flow")
             # Otherwise, `flows` contains a single maximal flow node.
             else:
                 [flow] = flows
         if not flow.spans(root):
-            raise Error("a descendant segment flow is expected",
-                        self.binding.mark)
+            raise Error("Expected a descendant segment flow")
         if (isinstance(code, LiteralCode) and
                 isinstance(code.domain, UntypedDomain)):
             if code.value is None:
             flow = FilteredFlow(flow, filter, flow.binding)
         if isinstance(self.binding, WeakSegmentBinding):
             if not root.spans(flow):
-                raise Error("a singular expression is expected",
-                            self.binding.mark)
+                raise Error("Expected a singular expression")
         return SegmentCode(root, flow, code, self.binding)
 
 
         # Generate the seed flow of the quotient.
         seed = self.state.relate(self.binding.seed)
         # Verify that the seed is a plural descendant of the parent flow.
-        if base.spans(seed):
-            raise Error("a plural expression is expected", seed.mark)
-        if not seed.spans(base):
-            raise Error("a descendant expression is expected",
-                        seed.mark)
+        with translate_guard(seed):
+            if base.spans(seed):
+                raise Error("Expected a plural expression")
+            if not seed.spans(base):
+                raise Error("Expected a descendant expression")
         # Encode the kernel expressions.
         kernels = [self.state.encode(binding)
                    for binding in self.binding.kernels]
         base = self.state.relate(self.binding.base)
         seed = self.state.relate(self.binding.seed)
         if not (seed.spans(base) and not base.spans(seed)):
-            raise Error("a plural expression is expected",
-                        self.binding.seed.mark)
+            with translate_guard(self.binding.seed):
+                raise Error("Expected a plural expression")
         return ClippedFlow(base, seed, self.binding.limit,
                            self.binding.offset, self.binding)
 
             return self.state.encode(self.base)
         # The default implementation complains that the conversion is
         # not admissible.
-        raise Error("cannot convert a value of type %s to %s"
-                    % (self.base.domain, self.domain),
-                    self.binding.mark)
+        raise Error("Cannot convert a value of type %s to %s"
+                    % (self.base.domain, self.domain))
 
 
 class ConvertUntyped(Convert):
         try:
             value = self.domain.parse(base.value)
         except ValueError, exc:
-            raise Error(str(exc), self.binding.mark)
+            # FIXME: `domain.parse()` should raise `Error`?
+            raise Error(str(exc))
         # Generate a new literal node with the converted value and
         # the target domain.
         code = LiteralCode(value, self.domain, self.binding)
 
     def __call__(self):
         # Override in subclasses for formulas that generate flow nodes.
-        raise Error("a flow expression is expected",
-                    self.binding.mark)
+        raise Error("a flow expression is expected")
 
 
 class EncodeSelection(Encode):

File src/htsql/core/tr/fn/bind.py

 from ...domain import (Domain, UntypedDomain, BooleanDomain, TextDomain,
         IntegerDomain, DecimalDomain, FloatDomain, DateDomain, TimeDomain,
         DateTimeDomain, EnumDomain, ListDomain, RecordDomain)
-from ...syn.syntax import (NumberSyntax, StringSyntax, IdentifierSyntax,
-        ComposeSyntax, ApplySyntax, OperatorSyntax, PrefixSyntax, GroupSyntax)
+from ...syn.syntax import (NumberSyntax, IntegerSyntax, StringSyntax,
+        IdentifierSyntax, ComposeSyntax, ApplySyntax, OperatorSyntax,
+        PrefixSyntax, GroupSyntax)
 from ..binding import (LiteralBinding, SortBinding, SieveBinding,
         FormulaBinding, CastBinding, ImplicitCastBinding, WrappingBinding,
         TitleBinding, DirectionBinding, QuotientBinding, AssignmentBinding,
         QueryBinding, Binding, BindingRecipe, ComplementRecipe, KernelRecipe,
         SubstitutionRecipe, ClosedRecipe)
 from ..bind import BindByName, BindingState
-from ...error import Error
+from ...error import Error, translate_guard, QuotePara, ErrorGuard
 from ..coerce import coerce
 from ..decorate import decorate
 from ..lookup import direct, expand, identify, guess_tag, lookup_command
                 message = "%s to %s arguments" % (min_args, max_args)
             else:
                 message = "%s or more arguments" % min_args
-            raise Error("function '%s' expects %s; got %s"
+            raise Error("Function '%s' expects %s; got %s"
                         % (self.name.encode('utf-8'),
-                           message, len(operands)),
-                        self.syntax.mark)
+                           message, len(operands)))
 
         for index, slot in enumerate(self.signature.slots):
             name = slot.name
                     recipes = expand(bound_value, with_syntax=True)
                     if slot.is_mandatory and (recipes is not None and
                                               not recipes):
-                        raise Error("at least one element is expected",
-                                    value.mark)
+                        with translate_guard(value):
+                            raise Error("Expected at least one element")
                     if recipes is None:
                         bound_value = [bound_value]
                     else:
         domain = coerce(*domains)
         if domain is None:
             if len(domains) > 1:
-                raise Error("cannot coerce values of types (%s)"
+                raise Error("Cannot coerce values of types (%s)"
                             " to a common type"
                             % (", ".join(str(domain)
-                                         for domain in domains)),
-                            self.syntax.mark)
+                                         for domain in domains)))
             else:
-                raise Error("a scalar value is expected",
-                            self.syntax.mark)
+                raise Error("Expected a scalar value")
         cast_arguments = {}
         for slot in self.signature.slots:
             name = slot.name
                     valid_families = "(%s)" % valid_families
                 if valid_families not in valid_types:
                     valid_types.append(valid_families)
-        hint = None
+        paragraphs = []
         if valid_types:
-            hint = "valid %s: %s" % (types, ", ".join(valid_types))
-        raise Error("%s cannot be applied to %s of %s %s"
-                    % (name, values, types, families),
-                    self.binding.mark, hint=hint)
+            paragraphs = [QuotePara("Valid %s" % types, "\n".join(valid_types))]
+        with ErrorGuard(*paragraphs):
+            raise Error("Cannot apply %s to %s of %s %s"
+                        % (name, values, types, families))
 
 
 def match(signature, *domain_vectors):
         seed = self.state.bind(op)
         recipes = expand(seed, with_syntax=True)
         if recipes is None:
-            raise Error("function '%s' expects an argument with a selector"
-                        % self.name.encode('utf-8'), op.mark)
+            with translate_guard(op):
+                raise Error("Function '%s' expects an argument with a selector"
+                            % self.name.encode('utf-8'))
         kernels = []
         for syntax, recipe in recipes:
             element = self.state.use(recipe, syntax, scope=seed)
             element = RescopingBinding(element, seed, element.syntax)
             domain = coerce(element.domain)
             if domain is None:
-                raise Error("quotient column must be scalar", element.mark)
+                with translate_guard(element):
+                    raise Error("Expected a scalar quotient column")
             element = ImplicitCastBinding(element, domain, element.syntax)
             kernels.append(element)
         quotient = QuotientBinding(self.state.scope, seed, kernels,
 
     def expand(self, base, title):
         if not isinstance(title, (StringSyntax, IdentifierSyntax)):
-            raise Error("function '%s' expects a string literal"
-                        " or an identifier" % self.name.encode('utf-8'),
-                        title.mark)
+            with translate_guard(title):
+                raise Error("Function '%s' expects a string literal"
+                            " or an identifier" % self.name.encode('utf-8'))
         base = self.state.bind(base)
         return TitleBinding(base, title, self.syntax)
 
 
     def parse(self, argument):
         try:
-            if not isinstance(argument, NumberSyntax):
+            if not isinstance(argument, IntegerSyntax):
                 raise ValueError
-            value = int(argument.value)
+            value = int(argument.text)
             if not (value >= 0):
                 raise ValueError
             if not isinstance(value, int):
                 raise ValueError
         except ValueError:
-            raise Error("function '%s' expects a non-negative integer"
-                        % self.name.encode('utf-8'), argument.mark)
+            with translate_guard(argument):
+                raise Error("Function '%s' expects a non-negative integer"
+                            % self.name.encode('utf-8'))
         return value
 
     def expand(self, seed, limit=None, offset=None):
 
     def parse(self, argument):
         try:
-            if not isinstance(argument, NumberSyntax):
+            if not isinstance(argument, IntegerSyntax):
                 raise ValueError
-            value = int(argument.value)
+            value = int(argument.text)
             if not (value >= 0):
                 raise ValueError
             if not isinstance(value, int):
                 raise ValueError
         except ValueError:
-            raise Error("function '%s' expects a non-negative integer"
-                        % self.name.encode('utf-8'), argument.mark)
+            with translate_guard(argument):
+                raise Error("Function '%s' expects a non-negative integer"
+                            % self.name.encode('utf-8'))
         return value
 
     def expand(self, limit, offset=None):
             if recipes is None:
                 domain = coerce(binding.domain)
                 if domain is None:
-                    raise Error("function '%s' expects a scalar"
-                                " expression" % self.name.encode('utf-8'),
-                                binding.mark)
+                    with translate_guard(binding):
+                        raise Error("Function '%s' expects a scalar"
+                                    " expression" % self.name.encode('utf-8'))
                 binding = ImplicitCastBinding(binding, domain, binding.syntax)
                 bindings.append(binding)
             else:
         for op in ops:
             assignment = self.state.bind(op, scope=binding)
             if not isinstance(assignment, AssignmentBinding):
-                raise Error("function '%s' expects an assignment"
-                            " expression" % self.name.encode('utf-8'),
-                            op.mark)
+                with translate_guard(op):
+                    raise Error("Function '%s' expects an assignment"
+                                " expression" % self.name)
             name, is_reference = assignment.terms[0]
             arity = None
             if is_reference:
         for op in rops:
             assignment = self.state.bind(op, scope=binding)
             if not isinstance(assignment, AssignmentBinding):
-                raise Error("function '%s' expects an assignment"
-                            " expression" % self.name.encode('utf-8'),
-                            op.mark)
+                with translate_guard(op):
+                    raise Error("Function '%s' expects an assignment"
+                                " expression" % self.name.encode('utf-8'))
             name, is_reference = assignment.terms[0]
             arity = None
             if is_reference:
     def expand(self):
         recipe = identify(self.state.scope)
         if recipe is None:
-            raise Error("cannot determine identity", self.syntax.mark)
+            raise Error("Cannot determine identity")
         return self.state.use(recipe, self.syntax)
 
 
         domains = [lop.domain] + [rop.domain for rop in rops]
         domain = coerce(*domains)
         if domain is None:
-            raise Error("cannot coerce values of types (%s)"
+            raise Error("Cannot coerce values of types (%s)"
                         " to a common type"
                         % (", ".join(str(domain)
-                                     for domain in domains)),
-                        self.syntax.mark)
+                                     for domain in domains)))
         lop = ImplicitCastBinding(lop, domain, lop.syntax)
         rops = [ImplicitCastBinding(rop, domain, rop.syntax) for rop in rops]
         if len(rops) == 1:
         domains = [lop.domain, rop.domain]
         domain = coerce(*domains)
         if domain is None:
-            raise Error("cannot coerce values of types (%s)"
+            raise Error("Cannot coerce values of types (%s)"
                         " to a common type"
                         % (", ".join(str(domain)
-                                     for domain in domains)),
-                        self.syntax.mark)
+                                     for domain in domains)))
         lop = ImplicitCastBinding(lop, domain, lop.syntax)
         rop = ImplicitCastBinding(rop, domain, rop.syntax)
         return FormulaBinding(self.state.scope,
         domains = [lop.domain, rop.domain]
         domain = coerce(*domains)
         if domain is None:
-            raise Error("cannot coerce values of types (%s)"
+            raise Error("Cannot coerce values of types (%s)"
                         " to a common type"
                         % (", ".join(str(domain)
-                                     for domain in domains)),
-                        self.syntax.mark)
+                                     for domain in domains)))
         lop = ImplicitCastBinding(lop, domain, lop.syntax)
         rop = ImplicitCastBinding(rop, domain, rop.syntax)
         is_comparable = Comparable.__invoke__(domain)
         if not is_comparable:
-            raise Error("values of type %s are not comparable"
-                        % domain, self.syntax.mark)
+            raise Error("Values of type %s are not comparable" % domain)
         return FormulaBinding(self.state.scope,
                               self.signature(self.relation),
                               coerce(BooleanDomain()),
     def match(self):
         operands = list(reversed(self.syntax.arguments))
         if len(operands) < 2:
-            raise Error("function '%s' expects 2 or more arguments;"
+            raise Error("Function '%s' expects 2 or more arguments;"
                         " got %s" % (self.name.encode('utf-8'),
-                                     len(operands)), self.syntax.mark)
+                                     len(operands)))
         predicates = []
         consequents = []
         alternative = None
         domain = coerce(*domains)
         if domain is None:
             if len(domains) > 1:
-                raise Error("cannot coerce values of types (%s)"
+                raise Error("Cannot coerce values of types (%s)"
                             " to a common type"
                             % (", ".join(str(domain)
-                                         for domain in domains)),
-                            self.syntax.mark)
+                                         for domain in domains)))
             else:
-                raise Error("a scalar value is expected",
-                            consequents[0].mark
-                            if consequents else alternative.mark)
+                with translate_guard(consequents[0]
+                                     if consequents else alternative):
+                    raise Error("Expected a scalar value")
         consequents = [ImplicitCastBinding(consequent, domain,
                                            consequent.syntax)
                        for consequent in consequents]
     def match(self):
         operands = list(reversed(self.syntax.arguments))
         if len(operands) < 3:
-            raise Error("function '%s' expects 3 or more arguments;"
+            raise Error("Function '%s' expects 3 or more arguments;"
                         " got %s" % (self.name.encode('utf-8'),
-                                     len(operands)), self.syntax.mark)
+                                     len(operands)))
         variable = None
         variants = []
         consequents = []
         domains = [variable.domain] + [variant.domain for variant in variants]
         domain = coerce(*domains)
         if domain is None:
-            raise Error("cannot coerce values of types (%s)"
+            raise Error("Cannot coerce values of types (%s)"
                         " to a common type"
                         % (", ".join(str(domain)
-                                     for domain in domains)),
-                        self.syntax.mark)
+                                     for domain in domains)))
         variable = ImplicitCastBinding(variable, domain, variable.syntax)
         variants = [ImplicitCastBinding(variant, domain, variant.syntax)
                     for variant in variants]
         domain = coerce(*domains)
         if domain is None:
             if len(domains) > 1:
-                raise Error("cannot coerce values of types (%s)"
+                raise Error("Cannot coerce values of types (%s)"
                             " to a common type"
                             % (", ".join(str(domain)
-                                         for domain in domains)),
-                            self.syntax.mark)
+                                         for domain in domains)))
             else:
-                raise Error("a scalar value is expected",
-                            consequents[0].mark
-                            if consequents else alternative.mark)
+                with translate_guard(consequents[0] if consequents
+                                     else alternative):
+                    raise Error("Expected a scalar value")
         consequents = [ImplicitCastBinding(consequent, domain,
                                            consequent.syntax)
                        for consequent in consequents]
         plural_base = None
         if recipes is not None:
             if len(recipes) != 1:
-                raise Error("function '%s' expects 1 argument; got %s"
-                            % (self.name.encode('utf-8'), len(recipes)),
-                            op.mark)
+                with translate_guard(op):
+                    raise Error("Function '%s' expects 1 argument; got %s"
+                                % (self.name.encode('utf-8'), len(recipes)))
             plural_base = op
             syntax, recipe = recipes[0]
             op = self.state.use(recipe, syntax)
         plural_base = None
         if recipes is not None:
             if len(recipes) != 1:
-                raise Error("function '%s' expects 1 argument; got %s"
-                            % (self.name.encode('utf-8'), len(recipes)),
-                            op.mark)
+                with translate_guard(op):
+                    raise Error("Function '%s' expects 1 argument; got %s"
+                                % (self.name.encode('utf-8'), len(recipes)))
             plural_base = op
             syntax, recipe = recipes[0]
             op = self.state.use(recipe, syntax)
         plural_base = None
         if recipes is not None:
             if len(recipes) != 1:
-                raise Error("function '%s' expects 1 argument; got %s"
-                            % (self.name.encode('utf-8'), len(recipes)),
-                            op.mark)
+                with translate_guard(op):
+                    raise Error("Function '%s' expects 1 argument; got %s"
+                                % (self.name.encode('utf-8'), len(recipes)))
             plural_base = op
             syntax, recipe = recipes[0]
             op = self.state.use(recipe, syntax)
         plural_base = None
         if recipes is not None:
             if len(recipes) != 1:
-                raise Error("function '%s' expects 1 argument; got %s"
-                            % (self.name.encode('utf-8'), len(recipes)),
-                            op.mark)
+                with translate_guard(op):
+                    raise Error("Function '%s' expects 1 argument; got %s"
+                                % (self.name.encode('utf-8'), len(recipes)))
             plural_base = op
             syntax, recipe = recipes[0]
             op = self.state.use(recipe, syntax)

File src/htsql/core/tr/fn/encode.py

 from ...adapter import Adapter, adapt, adapt_many, adapt_none
 from ...domain import UntypedDomain, BooleanDomain, IntegerDomain
 from ..encode import EncodeBySignature, EncodingState
-from ...error import Error
+from ...error import Error, translate_guard
 from ..coerce import coerce
 from ..binding import RootBinding, LiteralBinding, CastBinding
 from ..flow import (LiteralCode, ScalarUnit, CorrelatedUnit,
     adapt(AggregateSig)
 
     def aggregate(self, op, flow, plural_flow):
-        if plural_flow is None:
-            plural_units = [unit for unit in op.units
-                                 if not flow.spans(unit.flow)]
-            if not plural_units:
-                raise Error("a plural operand is expected", op.mark)
-            plural_flows = []
-            for unit in plural_units:
-                if any(plural_flow.dominates(unit.flow)
-                       for plural_flow in plural_flows):
-                    continue
-                plural_flows = [plural_flow
-                                 for plural_flow in plural_flows
-                                 if not unit.flow.dominates(plural_flow)]
-                plural_flows.append(unit.flow)
-            if len(plural_flows) > 1:
-                raise Error("cannot deduce an unambiguous"
-                            " aggregate flow", op.mark)
-            [plural_flow] = plural_flows
-        if flow.spans(plural_flow):
-            raise Error("a plural operand is expected", op.mark)
-        if not plural_flow.spans(flow):
-            raise Error("a descendant operand is expected",
-                        op.mark)
+        with translate_guard(op):
+            if plural_flow is None:
+                plural_units = [unit for unit in op.units
+                                     if not flow.spans(unit.flow)]
+                if not plural_units:
+                    raise Error("Expected a plural operand")
+                plural_flows = []
+                for unit in plural_units:
+                    if any(plural_flow.dominates(unit.flow)
+                           for plural_flow in plural_flows):
+                        continue
+                    plural_flows = [plural_flow
+                                     for plural_flow in plural_flows
+                                     if not unit.flow.dominates(plural_flow)]
+                    plural_flows.append(unit.flow)
+                if len(plural_flows) > 1:
+                    raise Error("Cannot deduce an unambiguous"
+                                " aggregate flow")
+                [plural_flow] = plural_flows
+            if flow.spans(plural_flow):
+                raise Error("Expected a plural operand")
+            if not plural_flow.spans(flow):
+                raise Error("Expected a descendant operand")
         # FIXME: handled by the compiler.
         #if not all(plural_flow.spans(unit.flow)
         #           for unit in plural_units):

File src/htsql/core/tr/rewrite.py

 
 from ..adapter import Utility, Adapter, adapt, adapt_many
 from ..domain import BooleanDomain
-from ..error import Error
+from ..error import Error, translate_guard
 from .coerce import coerce
 from .flow import (Expression, QueryExpr, SegmentCode, Flow, RootFlow,
         FiberTableFlow, QuotientFlow, ComplementFlow, MonikerFlow, ForkedFlow,
         if expression in self.rewrite_cache:
             return self.rewrite_cache[expression]
         # Apply `Rewrite` adapter.
-        replacement = rewrite(expression, self)
+        with translate_guard(expression):
+            replacement = rewrite(expression, self)
         # Cache the output.
         self.rewrite_cache[expression] = replacement
         return replacement
         # Verify that the kernel is not scalar.  We can't do it earlier
         # because since unmasking may remove fantom units.
         if all(not code.units for code in kernels):
-            raise Error("an empty or constant kernel is not allowed",
-                        self.flow.mark)
+            raise Error("Found an empty or constant kernel")
         # Unmask the seed against the quotient parent flow.
         seed = self.state.unmask(self.flow.family.seed, mask=self.flow.base)
         # Unmask the parent flow against the current mask.

File src/htsql/core/tr/stitch.py

 from ..adapter import Adapter, adapt, adapt_many
 from ..model import TableNode, ColumnArc, ChainArc
 from ..classify import normalize, localize
-from ..error import Error
+from ..error import Error, translate_guard
 from ..syn.syntax import IdentifierSyntax
 from .flow import (Flow, ScalarFlow, TableFlow, FiberTableFlow, QuotientFlow,
         ComplementFlow, MonikerFlow, ForkedFlow, LinkedFlow, ClippedFlow,
                     break
         # No primary key, we don't have other choice but to report an error.
         if connect_columns is None:
-            raise Error("unable to connect a table"
-                        " lacking a primary key", self.flow.mark)
+            with translate_guard(flow):
+                raise Error("Unable to connect a table"
+                            " lacking a primary key")
         # Generate joints that represent a connection by the primary key.
         flow = self.flow.inflate()
         for column in connect_columns:

File src/htsql/ctl/regress.py

         # to split the SQL data and to connect to the database, but we
         # never use it for executing HTSQL queries.
         from htsql import HTSQL
-        from htsql.core.connect import connect, DBError
+        from htsql.core.error import Error
+        from htsql.core.connect import connect
         from htsql.core.split_sql import split_sql
         try:
             app = HTSQL(self.input.connect)
             try:
                 connection = connect(with_autocommit=self.input.autocommit)
                 cursor = connection.cursor()
-            except DBError, exc:
+            except Error, exc:
                 return self.failed("*** failed to connect to the database:"
-                                   " %s" % exc)
+                                   "\n%s" % exc)
 
             # Execute the given SQL statements.
             for statement in statements: