Устранение неоднозначности LALR

Недавно я достаточно увлекся LALR, чтобы написать генератор LALR и Я пытаюсь построить для него грамматику в стиле java или c # (начало которой указано здесь).

Я знаю, что написание генератора синтаксического анализатора требует дополнительных усилий, например, заново изобретать колесо (почему бы не использовать Antlr?), Но моя цель - создать хобби-ОС, которая может компилироваться сама по себе, независимо от сторонней инструментальной цепочки. Моя проблема, правда, не в генераторе, а в грамматике.

Я сталкиваюсь с уменьшением / уменьшением двусмысленности с утверждениями и выражениями.

Я знаю, как разрешить определенные типы неоднозначности, такие как висячие и другие, но эти немногие из них не интуитивно понятны для меня, и они поставили меня в тупик.

Как лучше всего их решить? Кроме того, есть ли инструмент для создания прототипов, который я могу использовать для визуализации решения? Или мне следует вернуться к исходной точке и попробовать реализовать генератор парсера GLR для грамматики?

Эти заявления являются законными:

Generic.List<int> myVar1 = x + 4, myVar2; // stmt -> var-decl ;
                                          // var-decl -> type-name var-decl-list

t = 99;                           // simple-stmt -> assign

i++;                              // simple-stmt -> incr-decr
                                  // incr-decr -> primary-expr ++

json.deserialize<list<int>>(obj); // simple-stmt -> call
                                  // call -> primary-expr ( params )
                                  // ...  -> primary-expr . basic-name ( params )
                                  // ...  -> basic-name . basic-name ( params )

Вот как это устроено:

basic-name : ident < type-list >
           | ident

nested-name : nested-name . basic-name
            | basic-name

basic-type : int | bool | ...

type-name : nested-name
          | basic-type

stmt : var-decl ;
     | simple-stmt ;
     | ...

var-decl : type-name var-decl-list

var-decl-list : var-decl-list , var-init
              | var-init

var-init : ident assign-op expression
         | ident

simple-stmt : assign
            | call
            | incr-decr

expr : assign-expr

assign-expr : assign
            | cond-expr

assign : unary-expr assign-op expr

...
rel-expr : rel-expr < shift-expr
         ...
         | shift-expr

...
unary-expr : unary-op primary-expr
           | primary-expr

unary-op : + - ! ~ ++ --  // Prefix operators
         | ( type-name )  // Conversion operator

primary-expr : call
             | primary-expr . basic-name
             | primary-expr ++
             | ( expr )
             ...
             | basic-name

call : primary-expr ( params )

incr-decr : primary-expr ++
          | -- primary-expr
          | ...

Поэтому, когда синтаксический анализатор ожидает оператор, ядро ​​набора элементов * LR (k) равно method-body -> { * stmts-opt }, а полный набор элементов для состояния выглядит примерно так:

method-body -> { * stmts-opt }
stmts-opt -> * stmts
stmts-opt -> *
stmts -> * stmts stmt
stmt -> * var-decl ;
stmt -> * simple-stmt ;
var-decl -> * type-name var-decl-list
simple-stmt -> * assign
simple-stmt -> * call
simple-stmt -> * incr-decr
type-name -> * nested-name
type-name -> * basic-type
nested-name -> * nested-name . basic-name
nested-name -> * basic-name
basic-name -> * ident < type-list >
basic-name -> * ident
assign -> * unary-expr assign-op expr
unary-expr -> * unary-op primary-expr
unary-expr -> * primary-expr
unary-op -> * ( typename )
unary-op -> * ! | ~ | ...
primary-expr -> * call
primary-expr -> * primary-expr . basic-name
primary-expr -> * primary-expr ++
primary-expr -> * basic-name
primary-expr -> * ( expr )
call -> * primary-expr ( params )
incr-decr -> * primary-expr ++
...

При смещении идентификатора следующее состояние:

basic-name -> ident *
basic-name -> ident * < type-list >

который разбирается или сокращается и приводит к следующему состоянию:

nested-name -> basic-name *
primary-expr -> basic-name *

Возможный конфликт. В родительском состоянии предварительный просмотр не помогает, поскольку в nested-name и primary-expr есть точка. Ой, хорошо, давай попробуем сократить по вложенному имени:

type-name -> nested-name *
nested-name -> nested-name * . basic-name

Здесь не на что смотреть ... А как насчет того, чтобы вместо этого уменьшить на primary-expr:

unary-expr -> primary-expr *
primary-expr -> primary-expr * . basic-name
primary-expr -> primary-expr * ++
call -> primary-expr * ( params )
incr-decr -> primary-expr * ++
...

Теперь, когда мы сдвигаем ++, мы получаем:

primary-expr -> primary-expr ++ *
incr-decr -> primary-expr ++ *

... еще один конфликт уменьшения-уменьшения.

Попробуем сместить ( вместо ident:

primary-expr -> ( * expr )
unary-op -> ( * type-name )
expr -> * assign-expr
assign-expr -> * assign
assign-expr -> * cond-expr
assign -> * unary-expr assign-op expr
unary-expr -> * unary-op primary-expr
unary-expr -> * primary-expr
unary-op -> * ( typename )
unary-op -> * ! | ~ | ...
primary-expr -> * call
primary-expr -> * primary-expr . basic-name
primary-expr -> * primary-expr ++
primary-expr -> * basic-name
primary-expr -> * ( expr )
call -> * primary-expr ( params )
cond-expr -> * ...
...
rel-expr -> * rel-expr < shift-expr
rel-expr -> * shift-expr
...
type-name -> * nested-name
type-name -> * basic-type
nested-name -> * nested-name . basic-name
nested-name -> * basic-name
basic-name -> * ident < type-list >
basic-name -> * ident

Те же проблемы, когда вы перекладываете ident или ( в стек.

Это как раз те, с которыми я пока сталкивался. Поскольку basic-name имеет приоритет над rel-expr, я предполагаю, что что-то вроде x < n будет интерпретировано как basic-name -> ident < type-list *, и выйдет ошибка, если это действительно было реляционное выражение.

Мой мозг достиг точки, когда мне действительно нужна помощь.


person Josh Wyant    schedule 09.08.2015    source источник
comment
Реализует ли ваш LALR-генератор объявления приоритета, подобные бизону? Если нет, то как нам интерпретировать, поскольку основное-имя имеет приоритет над rel-expr ...?   -  person rici    schedule 10.08.2015
comment
@rici Поскольку rel-expr извлекает basic-name за очень много шагов, я говорю, что он имеет более высокий приоритет.   -  person Josh Wyant    schedule 10.08.2015
comment
Для меня это не имеет никакого смысла. Тот факт, что A является производным B, не влияет на решение алгоритма LR о сдвиге или сокращении.   -  person rici    schedule 10.08.2015
comment
@rici Может, я все еще не совсем понимаю. Поскольку существует только один символ просмотра вперед, чтобы сместить маркер < для rel-expr, мы должны уменьшить ident до basic-name, а затем до rel-expr, чтобы перейти в состояние rel-expr -> rel-expr * < shift-expr. Если мы это сделаем, даже если полный ввод равен x < n > ( ), мы уже уменьшили до rel-expr: rel-expr * < n > (), и мы не сможем получить < type-list > из следующих 3 токенов, потому что для этого мы должны получить basic-name -> ident < type-list > * на стек, а не basic-name -> rel-expr < type-list > *.   -  person Josh Wyant    schedule 10.08.2015
comment
Это, безусловно, сдвиг в сторону уменьшения конфликта. Вполне возможно, что это даже двусмысленность.   -  person rici    schedule 10.08.2015
comment
Я не думаю, что с текущей грамматикой возможно иметь состояние, которое включает в себя 2 элемента ядра, rel-expr -> ident * < shift-expr и basic-name -> ident * < type-list >, потому что первый не является реальным продуктом.   -  person Josh Wyant    schedule 10.08.2015
comment
Позвольте нам продолжить это обсуждение в чате.   -  person Josh Wyant    schedule 10.08.2015


Ответы (1)


В вашем сообщении есть несколько вопросов, что делает его не совсем идеальным для SO. Но я постараюсь высказать некоторые мысли по каждому из них. На мой взгляд, у вас есть три проблемы:

  1. Отличие выражений операторов от выражений, которые не являются операторами.

  2. Анализ иерархически названных типов в объявлении без конфликта с выражениями доступа к полю в выражениях выражений

  3. Различия между использованием в качестве оператора сравнения и скобки шаблона.


1. Отличие выражений операторов от выражений, которые не являются операторами.

Насколько я понимаю, желательно разрешить только выражения инструкций, которые имеют (или потенциально имеют) какой-то побочный эффект: присваивания, инкрементные мутации и вызовы подпрограмм. Грубо говоря, это соответствует Java, грамматика которой включает:

StatementExpression:
  Assignment
  PreIncrementExpression
  PreDecrementExpression
  PostIncrementExpression
  PostDecrementExpression
  MethodInvocation
  ClassInstanceCreationExpression

Каждая из альтернатив, перечисленных для StatementExpression также, появляется где-то в дереве вывода для Expression, где они были исключены из списков возможностей. Вот очень сжатый образец:

Expression:
  LambdaExpression
  AssignmentExpression

AssignmentExpression:
  ConditionalExpression
  Assignment

Assignment:
  LeftHandSide AssignmentOperator Expression

...

UnaryExpression:
  PreIncrementExpression
  + UnaryExpression
  UnaryExpressionNotPlusMinus

PreIncrementExpression:
  ++ UnaryExpression

UnaryExpressionNotPlusMinus:
  PostfixExpression
  ~ UnaryExpression

PostfixExpression:
  Primary
  ExpressionName
  PostIncrementExpression

PostIncrementExpress:
  PostfixExpression ++

Обратите внимание на то, что нетерминалы, используемые в правой части ExpressionStatement, имеют специальный регистр на каждом уровне приоритета. В грамматике C ++, которая не ограничивает, какие выражения могут быть операторами, нет необходимости в отдельном Assignment нетерминальном:

assignment-expression:
  conditional-expression
  logical-or-expression assignment-operator initializer-clause

Но в Java это не сработает. Ему необходимо создать дополнительный нетерминал, который не является производным ConditionalExpression, именно для того, чтобы позволить Assignment быть Statement, а AssignmentExpression быть Expression.

2. Анализ иерархически названных типов в объявлении без конфликта с выражениями доступа к полю в выражениях.

Как и в предыдущем случае, здесь необходимо указать иерархические имена (которые должны начинаться с basic-name) из других форм выражений доступа к полю, которые могут начинаться с любого (другого) primary-expr. Первые могут быть именами типов или первичными выражениями; последние могут быть только именами типов. Чтобы сделать это различие, нам нужно разделить primary-expr производство:

primary-expr : field-access-expr
             | nested-name

non-field-access-expr:
               call
             | post-increment-expression  // was primary-expr ++
             | ( expr )
             ...

field-access-expr :
               non-field-access-expr
             | field-access-expr . basic-name

3. Различие между использованием как оператора сравнения и как скобки шаблона.

В отличие от двух других проблем, это может быть двусмысленность в языке. В C ++, например, скобки в шаблоне определенно неоднозначны; их можно разрешить, только зная (или получив информацию), является ли конкретное имя именем шаблона или нет. В Java, с другой стороны, неоднозначность решается за счет того, что иногда требуется, чтобы параметры типа находились перед общим именем. Например:

ConstructorDeclarator:
  [TypeParameters] SimpleTypeName ( [FormalParameterList] )

or

MethodInvocation:
  Primary . [TypeArguments] Identifier ( [ArgumentList] )

В C # еще есть другое правило, которое требует смотреть на символ, следующий за , который потенциально соответствует открывающему . Алгоритм описан в п. 7.6.4.2 руководства по C #; Понятия не имею, как это реализовать в парсере LALR (1).

person rici    schedule 10.08.2015