Op/3 binary with lower precedence somehow evaluated before unaries with higher precedence

I am a little confused by the op/3 behavior.

:- op(100, fy, xxx).
:- op(200, xfy, yyy).
:- op(100, yf, zzz).

goal :- display(xxx yyy zzz), nl.

Why the above prints yyy(xxx,zzz) ?

I would expect it to print zzz(xxx(yyy)).

Did you mean this? (Although it gives the same result):

:- op(100, yf, zzz).

Anyway, with this definition (zzz is a postfix operator; if I use your definition for zzz, I get a syntax error):

?- X = (xxx 1 zzz), write_canonical(X), nl.
zzz(xxx(1))
X = xxx 1 zzz.

Ah yes. Going to edit the question.
Changed zzz to be postfix.

I get this issue in

SWI-Prolog version 9.2.8 for x86_64-darwin

and in

SWI-Prolog version 9.3.14 for fat-darwin

According to the ISO rules, this is a syntax error. SWI-Prolog and various others try to be a bit more flexible. Roughly, we have

  • xxx. Looks for an operant
  • finds yyy. yyy has higher priority and it is not the end of the term, so it decides xxx is an operant for yyy and looks for its 2nd argument for yyy, which it finds in zzz.

The ISO rules are strict, but require parenthesis in many places where humans think it is not really ambiguous. Many real Prolog systems are more relaxed. This typically works fine for many of the obvious cases, but at some point things get ambiguous :frowning: Unfortunately no two systems apply the same rules …

For reference:

/* Ciao 1.24 */
?- op(100, fy, xxx), op(200, xfy, yyy), op(100, yf, zzz).

yes
?- X = (xxx yyy zzz), write_canonical(X), nl.
xxx(zzz(yyy))

X = xxx yyy zzz ? 

History, I guess. I had a discussion with SICStus several years ago, trying to sync. That did not lead to a specification and I failed to reverse engineer the SICStus rules.

I would love a new set of standard rules that is less restrictive than ISO, but probably more restrictive than SICStus. What I learned is that it is basically non-deterministic Prolog code that commits on the first solution.

For now, I have no plans to change things unless we can come up (PIP) with a decent proposal that is carried by several other systems such that I can motivate the change as “standard”.

AFAIK, SICStus Prolog uses a backtracking parser. SWI-Prolog does not. It uses a parser technique that I got from some book I’ve forgotten about long ago that is some form of shift-reduce parsing. At some point, your are stuck and have to drop the operator property of one of the atoms or raise a syntax error. Here, we have fx xfy. These cannot both be operators. At least in theory, one can describe the logic.

A fully backtracking parser accepts more. We’d have to define what do do in case there are multiple parses possible though. AFAIK, SICStus commits to the first, so the order generated by the implementation matters. This is, again AFAIK, not well described though.

The bottom line remains that both parses are IMO equally justifiable. The standard says we should reject this input. Ideally we’d come up with understandable rules that disambiguates ambiguous (ISO errornous) input. Did I miss some de-facto standard algorithm that is in use by a good deal of the other Prolog systems? If so, I’m happy to adopt it. Preferably after agreement.

Well, it is a real world code in the sence that I have picked up the compiler and started to write down a demonstratory example. My goal was not producing a compiler bug report, but rather to demonstrate an applicability of precedence parsing to some NLP tasks.

But then I realised that for the next sentence I get a wrong parse.

The code is the following.

  
:- op(100, fy, березовой).
:- op(100, fy, высокий).
:- op(100, fy, даже).
:- op(100, fy, еще).
:- op(100, fy, сам).
:- op(100, fy, каждый).
:- op(100, fy, каждого).
:- op(100, fy, много).
:- op(100, fy, надо).
:- op(100, fy, небольшой).
:- op(100, fy, не).
:- op(100, fy, ни).
:- op(100, fy, огурцовой).
:- op(100, fy, одном).
:- op(100, fy, один).
:- op(100, fy, очень).
:- op(100, fy, ростом).
:- op(100, fy, сказочном).
:- op(100, fy, цветочным).
:- op(100, fy, этот).

% postfix
:- op(100, yf, ведь).
:- op(100, yf, вовсе).
:- op(100, yf, бы).

:- op(100, fy, в).
:- op(100, fy, вокруг).
:- op(100, fy, за).
:- op(100, fy, из).
:- op(100, fy, с).
:- op(100, fy, на).
:- op(100, fy, по).
:- op(100, fy, у).
:- op(100, fy, через).

:- op(120, yf, васильков).
:- op(120, yf, колокольчиков).
:- op(120, yf, ромашек).
:- op(120, yf, цветов).
:- op(120, yf, огурцов).
:- op(120, yf, ручья).

:- op(180, xfy, @).

:- op(190, fy, лазить).
:- op(190, fy, тащить).
:- op(190, fy, пилить).
:- op(190, fy, собирать).
:- op(190, fy, сорвать).

:- op(200, xfy, были).
:- op(200, xfy, был).
:- op(200, xfy, было).
:- op(200, xfy, делали).
:- op(200, xfy, жили).
:- op(200, xfy, называли).
:- op(200, xfy, назывались).
:- op(200, xfy, назывался).
:- op(200, xfy, переплывали).

:- op(200, xfy, приходилось).
:- op(200, xfy, росли).
:- op(200, xfy, росло).
:- op(200, xfy, стоял).
:- op(200, xfy, смог).
:- op(200, xfy, ходили).

:- op(300, yfx, зпт).

:- op(300, fy, что).

:- op(400, yfx, потому).
:- op(400, yfx, да).
:- op(400, yfx, и).
:- op(400, yfx, двтч).

:- op(500, fy, а).

goal :-  
    display(в одном сказочном городе жили коротышки), nl,
    display(коротышками @ их называли 0 потому что они были очень маленькие), nl,
    display(каждый коротышка был ростом с небольшой огурец), nl,
    display(в городе @ у них было очень красиво), nl,
    display(вокруг каждого дома росли цветы двтч маргаритки зпт ромашки зпт одуванчики), nl,
    display(там @ даже улицы назывались именами цветов двтч улица колокольчиков зпт аллея ромашек зпт бульвар васильков), nl,
    display(а сам город назывался цветочным городом), nl,
    display(он стоял на берегу ручья), nl,
    display(этот ручей @ коротышки называли огурцовой рекой потому что по берегам ручья росло много огурцов), nl,

    display(за рекой был лес), nl,
    display(коротышки делали из березовой коры @ лодочки зпт 0 переплывали через реку и 0 ходили в лес @ (за ягодами зпт за грибами зпт за орехами)), nl,
    display(собирать ягоды было трудно потому что коротышки ведь были крошечные), nl,
    display(0 да /*еще*/ тащить с собой @ пилу), nl,
    display(а за орехами /*и вовсе*/ приходилось лазить на высокий куст да /*еще*/ тащить с собой @ пилу), nl,
    display((ни один коротышка) @ (не смог бы) @ (сорвать орех @ руками)), nl,
    display(их @ надо было пилить пилой), nl.

bug :- display(не смог бы). % WTF??? смог(не, бы) Why ???

The program prints out the following if you are interested

жили(в(одном(сказочном(городе))),коротышки)
потому(называли(@(коротышками,их),0),что(были(они,очень(маленькие))))
был(каждый(коротышка),ростом(с(небольшой(огурец))))
было(@(в(городе),у(них)),очень(красиво))
двтч(росли(вокруг(каждого(дома)),цветы),зпт(зпт(маргаритки,ромашки),одуванчики))
двтч(назывались(@(там,даже(улицы)),цветов(именами)),зпт(зпт(колокольчиков(улица),ромашек(аллея)),васильков(бульвар)))
а(назывался(сам(город),цветочным(городом)))
стоял(он,ручья(на(берегу)))
потому(называли(@(этот(ручей),коротышки),огурцовой(рекой)),что(росло(ручья(по(берегам)),огурцов(много))))
был(за(рекой),лес)
и(зпт(делали(коротышки,@(из(березовой(коры)),лодочки)),переплывали(0,через(реку))),ходили(0,@(в(лес),зпт(зпт(за(ягодами),за(грибами)),за(орехами)))))
потому(было(собирать(ягоды),трудно),что(были(ведь(коротышки),крошечные)))
да(0,тащить(@(с(собой),пилу)))
а(да(приходилось(за(орехами),лазить(на(высокий(куст)))),тащить(@(с(собой),пилу))))
@(ни(один(коротышка)),@(смог(не,бы),сорвать(@(орех,руками))))
было(@(их,надо),пилить(пилой))

Yeah, I was under impression there is some left to right ordering of op/3 applications, but looks like I was wrong.

Unfortunately the parsing strategy is not described in the docs (and of course there are all kinds of good reasons for that). So I had some intuitive model of the parse, but what exactly happens when op/3 is used is unclear from the docs.

Again, for reference:

/* Ciao 1.24 */

?- X = 'не'('бы'('смог')).

X = не смог бы ? 

yes
?- X = 'бы'('не'('смог')).

X = (не смог)бы ? 

yes
?- X = не смог бы.

X = не смог бы ? 

yes
?- X = (не смог)бы.

X = (не смог)бы ? 

yes
?- 

I don’t recommend putting the code into any test codebase.
It is an expert from an existing book.

This, as I said, is a bug. Pushed a fix. Any combination of operators that is valid according to the ISO standard should result in the term dictated by ISO. The issue of this topic is what to do with combinations that are invalid according to ISO as the given example is invalid.