Skip to content

Commit

Permalink
Refactor naming of parser source span (#7071)
Browse files Browse the repository at this point in the history
  • Loading branch information
aljazerzen committed Mar 23, 2024
1 parent 870bda8 commit bb1d6da
Show file tree
Hide file tree
Showing 87 changed files with 1,201 additions and 1,204 deletions.
10 changes: 5 additions & 5 deletions edb/common/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@

import parsing

from edb.common import context as pctx, debug
from edb.common import debug, span

ParserContext = pctx.ParserContext
Span = span.Span

logger = logging.getLogger('edb.common.parsing')

Expand Down Expand Up @@ -99,11 +99,11 @@ def for_lex_token(mcls, token):


class Token(parsing.Token, metaclass=TokenMeta):
def __init__(self, val, clean_value, context=None):
def __init__(self, val, clean_value, span=None):
super().__init__()
self.val = val
self.clean_value = clean_value
self.context = context
self.span = span

def __repr__(self):
return '<Token %s "%s">' % (self.__class__._token, self.val)
Expand Down Expand Up @@ -151,7 +151,7 @@ def __new__(mcls, name, bases, dct):
attr = lambda self, *args, meth=attr: meth(self, *args)
attr.__doc__ = doc

a = pctx.has_context(attr)
a = span.wrap_function_to_infer_spans(attr)

a.__doc__ = attr.__doc__
a.inline_index = inline_index
Expand Down
145 changes: 72 additions & 73 deletions edb/common/context.py → edb/common/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

from __future__ import annotations

from typing import List
import re
import bisect

Expand All @@ -46,8 +47,10 @@
NEW_LINE = re.compile(br'\r\n?|\n')


class ParserContext(markup.MarkupExceptionContext):
title = 'Source Context'
class Span(markup.MarkupExceptionContext):
'''
Parser Source Context
'''

def __init__(self, name, buffer, start: int, end: int, document=None, *,
filename=None, context_lines=1):
Expand All @@ -62,6 +65,15 @@ def __init__(self, name, buffer, start: int, end: int, document=None, *,
assert start is not None
assert end is not None

@classmethod
def empty(cls) -> Span:
return Span(
name='<empty>',
buffer='',
start=0,
end=0,
)

def __getstate__(self):
dic = self.__dict__.copy()
dic['_points'] = None
Expand Down Expand Up @@ -121,71 +133,61 @@ def as_markup(cls, self, *, ctx):
return me.lang.ExceptionContext(title=self.title, body=[tbp])


def _get_context(items, *, reverse=False):
def _get_span(items, *, reverse=False):
ctx = None

items = reversed(items) if reverse else items
# find non-empty start and end
#
for item in items:
if isinstance(item, (list, tuple)):
ctx = _get_context(item, reverse=reverse)
ctx = _get_span(item, reverse=reverse)
if ctx:
return ctx
else:
ctx = getattr(item, 'context', None)
ctx = getattr(item, 'span', None)
if ctx:
return ctx

return None


def empty_context():
"""Return a dummy context that points to an empty string."""
return ParserContext(
name='<empty>',
buffer='',
start=0,
end=0,
)


def get_context(*kids):
start_ctx = _get_context(kids)
end_ctx = _get_context(kids, reverse=True)
def get_span(*kids: List[ast.AST]):
start_ctx = _get_span(kids)
end_ctx = _get_span(kids, reverse=True)

if not start_ctx:
return None

return ParserContext(
return Span(
name=start_ctx.name,
buffer=start_ctx.buffer,
start=start_ctx.start,
end=end_ctx.end,
)


def merge_context(ctxlist):
ctxlist.sort(key=lambda x: (x.start, x.end))
def merge_spans(spans: List[Span]) -> Span:
spans.sort(key=lambda x: (x.start, x.end))

# assume same name and buffer apply to all
#
return ParserContext(
name=ctxlist[0].name,
buffer=ctxlist[0].buffer,
start=ctxlist[0].start,
end=ctxlist[-1].end,
return Span(
name=spans[0].name,
buffer=spans[0].buffer,
start=spans[0].start,
end=spans[-1].end,
)


def force_context(node, context):
if hasattr(node, 'context'):
ContextPropagator.run(node, default=context)
node.context = context
def infer_span_from_children(node, span: Span):
if hasattr(node, 'span'):
SpanPropagator.run(node, default=span)
node.span = span


def has_context(func):
"""Provide automatic context for Nonterm production rules."""
def wrap_function_to_infer_spans(func):
"""Provide automatic span for Nonterm production rules."""
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
obj, *args = args
Expand All @@ -196,36 +198,31 @@ def wrapper(*args, **kwargs):
#
arg = args[0]
if getattr(arg, 'val', None) is obj.val:
if hasattr(arg, 'context'):
obj.context = arg.context
if hasattr(obj.val, 'context'):
obj.val.context = obj.context
if hasattr(arg, 'span'):
obj.span = arg.span
if hasattr(obj.val, 'span'):
obj.val.span = obj.span
return result

# Avoid mangling any existing context.
if getattr(obj, 'context', None) is None:
obj.context = get_context(*args)
# Avoid mangling existing span
if getattr(obj, 'span', None) is None:
obj.span = get_span(*args)

# we have the context for the nonterminal, but now we need to
# enforce context in the obj.val, recursively, in case it was
# we have the span for the nonterminal, but now we need to
# enforce span in the obj.val, recursively, in case it was
# a complex production with nested AST nodes
#
force_context(obj.val, obj.context)
infer_span_from_children(obj.val, obj.span)
return result

return wrapper


class ContextVisitor(ast.NodeVisitor):
pass


class ContextPropagator(ContextVisitor):
"""Propagate context from children to root.
class SpanPropagator(ast.NodeVisitor):
"""Propagate span from children to root.
It is assumed that if a node has a context, all of its children
also have correct context. For a node that has no context, its
context is derived as a superset of all of the contexts of its
It is assumed that if a node has a span, all of its children
also have correct span. For a node that has no span, its
span is derived as a superset of all of the spans of its
descendants.
"""

Expand All @@ -234,40 +231,42 @@ def __init__(self, default=None):
self._default = default

def container_visit(self, node):
ctxlist = []
span_list = []
for el in node:
if isinstance(el, ast.AST) or typeutils.is_container(el):
ctx = self.visit(el)
span = self.visit(el)

if isinstance(ctx, list):
ctxlist.extend(ctx)
if isinstance(span, list):
span_list.extend(span)
else:
ctxlist.append(ctx)
return ctxlist
span_list.append(span)
return span_list

def generic_visit(self, node):
# base case: we already have context
#
if getattr(node, 'context', None) is not None:
return node.context
# base case: we already have span
if getattr(node, 'span', None) is not None:
return node.span

# we need to derive context based on the children
#
ctxlist = self.container_visit(v[1] for v in ast.iter_fields(node))
# we need to derive span based on the children
span_list = self.container_visit(v for _, v in ast.iter_fields(node))

if None in span_list:
node.dump()
print(list(ast.iter_fields(node)))

# now that we have all of the children contexts, let's merge
# now that we have all of the children spans, let's merge
# them into one
#
if ctxlist:
node.context = merge_context(ctxlist)
if span_list:
node.span = merge_spans(span_list)
else:
node.context = self._default
node.span = self._default

return node.context
return node.span


class ContextValidator(ContextVisitor):
class SpanValidator(ast.NodeVisitor):
def generic_visit(self, node):
if getattr(node, 'context', None) is None:
raise RuntimeError('node {} has no context'.format(node))
if getattr(node, 'span', None) is None:
raise RuntimeError('node {} has no span'.format(node))
super().generic_visit(node)
10 changes: 5 additions & 5 deletions edb/edgeql/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@
import typing

from edb.common import enum as s_enum
from edb.common import ast, parsing
from edb.common import ast, span

from . import qltypes

Span = span.Span

DDLCommand_T = typing.TypeVar(
'DDLCommand_T',
Expand Down Expand Up @@ -85,15 +86,14 @@ def to_edgeql(self) -> str:

class Base(ast.AST):
__abstract_node__ = True
__ast_hidden__ = {'context', 'system_comment'}
__ast_hidden__ = {'span', 'system_comment'}
__rust_ignore__ = True

context: typing.Optional[parsing.ParserContext] = None
span: typing.Optional[Span] = None

# System-generated comment.
system_comment: typing.Optional[str] = None

# parent: typing.Optional[Base]

def dump_edgeql(self) -> None:
from edb.common.debug import dump_edgeql

Expand Down
Loading

0 comments on commit bb1d6da

Please sign in to comment.