Skip to content

Allowed memory size of 134217728 bytes exhausted #12

@npgeorgiou-zz

Description

@npgeorgiou-zz

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 4096 bytes) in /home/parser-generator/vendor/antlr/antlr4-php-runtime/src/Atn/ParserATNSimulator.php on line 2036

Hi, I am trying to use the PHP target, but it throws the above error, while parsing a file using a grammar. The same file and same grammar works fine on the Java target. If I remove the PHP process memory limit, it succeeds after a few seconds, but thats not right, as the file is very simple. Also, it is just a particular rule that creates this memory overuse, every other rule seems to be parsing fine.

I tried to make it so you can relatively easy reproduce the error.
The repo is https://github.com/npgeorgiou/say, and the PHP target experiment is in the parser-generator directory, which also has instructions on how to reproduce the error: https://github.com/npgeorgiou/say/tree/main/parser-generator

Let me know if I can do anything else to make it easier for you.

Activity

npgeorgiou-zz

npgeorgiou-zz commented on Apr 18, 2021

@npgeorgiou-zz
Author

Ok, I have more info. This memory bug happens when a subrule starts with itself. For example, here I have a section of my grammar:

expression:
|SAY expression # Say
|EXCLAMATION expression # Prefix
|expression EXCLAMATION # Postfix
;

say !foo will be parsed without issues
say foo! will run out of memory or, if I give it unlimited memory, will be very very slow. Of course this cannot work for big files that contain a few of these expresions.

marcospassos

marcospassos commented on Apr 18, 2021

@marcospassos
Collaborator

Hi @npgeorgiou!

Thank you for reporting the issue. It seems like a recursion-related bug. I don't have time to track down the problem right now. If you have time to investigate this, PR’s are always welcome.

npgeorgiou-zz

npgeorgiou-zz commented on Apr 18, 2021

@npgeorgiou-zz
Author

I would love to, but I am not smart enough for that. What I can offer to any brave adventurer is these:

A grammar:

expression:
    IDENTIFIER                                    # Identifier
    |param_list? ARROW expression+                # Function_literal
    |EXCLAMATION expression                       # Prefix
    |expression EXCLAMATION                       # Postfix
    |SAY expression                               # Say
    |<assoc=right> expression ASSIGN expression   # Assignment
    |PO expression PC                             # P_expression
;

param_list: params+=function_param (COMMA params+=function_param)* COMMA?;
function_param: IDENTIFIER (ASSIGN defaultValue=expression)?;

A file:

is_leap << (year ->
    say foo!
    say foo!
    say foo!
    say foo!
    say foo!
    say foo!
    say foo!
    say foo!
    say foo!
    say foo!
    say foo!
)

Traveller, observe how the postfix **foo!**s are creating this memory error. The more of them, the slower it goes until it runs out of memory.

Also, observe how changing the postixes to prefixes (!foos) eliminates the bug.
Then, oberve how taking the **foo!**s out of the is_leap function body eliminates the bug as well.
Finally, oberve how, weirdly enough changing the

function_param: IDENTIFIER (ASSIGN defaultValue=expression)?;
to
function_param: IDENTIFIER;

improves the situation, although with enough **foo!**s it appears again.

May the gods of the crossroads and the in-betweens be with you.

kaby76

kaby76 commented on Dec 17, 2022

@kaby76

It seems I ran into a similar bug with the aql grammar with input for.aql. I'm trying to track it down, but also ran into #33, which makes the tracing impossible to use.

kaby76

kaby76 commented on Dec 17, 2022

@kaby76

In the driver, I turned on the new "tracing" features in v4.11.2 (current dev tip). I also edited the source to output a newline for the standard trace parser visitor.

PHP:

(before parse call)
$parser->setTrace(true);
Antlr\Antlr4\Runtime\Atn\ParserATNSimulator::$traceAtnSimulation = true;

CSharp:

(before parse call)
parser.Trace = true;
ParserATNSimulator.trace_atn_sim = true;

With tracing set, from the command line, the parse completes for PHP! Without trace set, the first call to AdaptivePredict() does not complete, and leads to out of memory exception. Note, regardless of trace set or not, in the XDebug/PHPStorm debugger, the run does not terminate! From the command line, with trace, there are ATN set differences between CSharp, which works fine, and PHP, which terminates because it's at the command line.

As for the diffs, it happens on the first "addDFAState".

PHP:

addDFAState new 0:[(297,1,[71 68 $]), (363,1,[225 126 74 68 $]), (226,1,[126 74 68 $]), (363,1,[290 121 68 $]), (291,1,[121 68 $]), (79,1,[68 $]), (297,2,[155 68 $]), (363,2,[225 126 158 68 $]), (226,2,[126 158 68 $]), (162,2,[68 $]), (169,2,[68 $]), (181,2,[68 $]), (193,2,[68 $]), (204,2,[68 $])]

CSharp:

addDFAState new 0:[(297,1,[71 68 $]), (363,1,[[225 126 74 68 $, 290 121 68 $]]), (226,1,[126 74 68 $]), (291,1,[121 68 $]), (79,1,[68 $]), (297,2,[155 68 $]), (363,2,[225 126 158 68 $]), (226,2,[126 158 68 $]), (162,2,[68 $]), (169,2,[68 $]), (181,2,[68 $]), (193,2,[68 $]), (204,2,[68 $])]

I don't know enough about the output to understand this, but it seems one is an aggregate because it contains two '[' in CSharp, but only one '[' for PHP. This sounds like the runtime is written with a misinterpretation of the data structure.

@parrt Is there a detailed description of this output?

I plan to add to the output the name of the type it is printing out (ATN, ATNSet, etc.). Again, my guess is that there is a data structure that isn't conforming to the expected type.

All of this point to some serious problems with PHP:

  • The ATN sets differ when executed from the command line.
  • PHP in the debugger vs command line differ in termination.
kaby76

kaby76 commented on Dec 17, 2022

@kaby76

Since I can't tell what a [ opens for a type (ATNConfig, ArrayPredicateConfig, ....), I decided to add identifiers to the "ToString()" output methods to tell me the object type it is trying to print out. I now am starting to see WTH is going on.

PHP:

addDFAState new 0:[atncs(ac297,1,[ac71 68 $]ac)ac, (ac363,1,[ac225 126 74 68 $]ac)ac, (ac226,1,[ac126 74 68 $]ac)ac, (ac363,1,[ac290 121 68 $]ac)ac, (ac291,1,[ac121 68 $]ac)ac, (ac79,1,[ac68 $]ac)ac, (ac297,2,[ac155 68 $]ac)ac, (ac363,2,[ac225 126 158 68 $]ac)ac, (ac226,2,[ac126 158 68 $]ac)ac, (ac162,2,[ac68 $]ac)ac, (ac169,2,[ac68 $]ac)ac, (ac181,2,[ac68 $]ac)ac, (ac193,2,[ac68 $]ac)ac, (ac204,2,[ac68 $]ac)ac]atncs

CSharp:

addDFAState new 0:[atncs(ac297,1,[ac71 68 $]ac)ac, (ac363,1,[ac[apc225 126 74 68 $, 290 121 68 $]apc]ac)ac, (ac226,1,[ac126 74 68 $]ac)ac, (ac291,1,[ac121 68 $]ac)ac, (ac79,1,[ac68 $]ac)ac, (ac297,2,[ac155 68 $]ac)ac, (ac363,2,[ac225 126 158 68 $]ac)ac, (ac226,2,[ac126 158 68 $]ac)ac, (ac162,2,[ac68 $]ac)ac, (ac169,2,[ac68 $]ac)ac, (ac181,2,[ac68 $]ac)ac, (ac193,2,[ac68 $]ac)ac, (ac204,2,[ac68 $]ac)ac]atncs

Notice the missing "[apc" tag. Whatever object PHP is printing, it is NOT an ArrayPredictionContext in PHP (but it is in CSharp) because I specifically modified the toString() method with tags. I will now debug toString() and see what the heck the object is.

@parrt Please, please, please add some kind of tagging system to note what type of object is being printed in the ATN trace output. I cannot tell what '[' opens.

parrt

parrt commented on Dec 17, 2022

@parrt
Member

hi @kaby76 thanks for the heads up. As usual an excellent analysis. The issue is that there are lots of different types that represent the same abstract concept of context. Not a bad idea, but the real issue here is that we don't have find enough granularity on the simulation trace. Is all of the output perfect up until that add the first DFA state? If so, then we need to add more output to the targets so that they identify why it is not generating the right stuff. Given the grammar, I can see that it is the left recursive stuff that's the problem. That will involve the precedence semantic predicates.

My head is stuck in something else at the moment so I don't have time to dig into this but maybe this gives you a bit of a clue? There's definitely a flaw in the ATN sim here. You might try reducing the offending rule to have one left recursive call and one non-recursive call. Also try using the recursive rule as the start symbol and then have a symbol above it. That could give a clue or at least a smaller test set.

kaby76

kaby76 commented on Dec 17, 2022

@kaby76

The first DFAState added between C# and PHP may be different.

C# in VC2022, for "D" at this line, first time hit, for dev branch, for.aql file input. The "configSet.configs" field doesn't even contain the same number of items. In C#, it's 13 elements.

2022-12-17 (5)

2022-12-17 (6)

2022-12-17 (3)

PHP in PHPStorm, at this line, the configSet.configs field has 14 items. This is bad.

2022-12-17 (4)

kaby76

kaby76 commented on Dec 17, 2022

@kaby76

CSharp and PHP code look completely different--missing "else" in PHP code. But, this is where the ArrayPredictionContext is create in CSharp, but it's never called in PHP.

So, there's more than one error likely.

kaby76

kaby76 commented on Dec 18, 2022

@kaby76

Found the problem. Or, in all likelihood, it may be only one of several.

In ATNConfigSet, there is a table called configLookup. It is a Dictionary<ATNConfig, ATNConfig> in C#, but a Set of ATNConfig in PHP. (Note, we've seen this difference in implementation before, between other targets.)

In C#, the code calls "GetOrAdd()", which is here. That table has a special comparer and hash function set for the class. The hash function that is executed is in class ConfigEqualityComparer, which has code that uses three fields of the ATNConfig.

Over in PHP, the code uses a generic Set implementation for $configLookup. That set is allocated here. Notice the hash function defined in this anonymous class here. That calls the standard hash function for ATNConfig. That computes the hash value using four fields--which is wrong!

I've verified that the call stack is indeed calling the wrong hash function for ATNConfig for this table in ParserATNSimulator. This is quite serious.

parrt

parrt commented on Dec 18, 2022

@parrt
Member

hahah. it's ALWAYS the hash function. @marcospassos looks like there might be an issue. We need a map from X->X not a set so we can reuse the same instance.

kaby76

kaby76 commented on Dec 19, 2022

@kaby76

I have an initial set of changes that seems to get past some of the parser tracing diffs.
diffs.txt

Essentially, as per note by @parrt I replaced the Set type for $configLookup in ATNConfigSet.php with Map as done with Java and CSharp. I also corrected the hash function/comparsion class for the map to be identical in CSharp. The Map interface needed some changes in the API for getOrAdd() and isEmpty()--which is just a hack to move past the first problem. It also contains some "print()'s" that end trace output with a new line ("echo()" does not write a newline!!). #33. People can wordsmith the "correct" design.

The trace is starting to now look better, getting past the diff with "addDFAState" in the output. But I still notice diffs. There are more problems.

32 remaining items

parrt

parrt commented on Dec 22, 2022

@parrt
Member

we could simply add a new flag to the runtime test mechanism that says to compare the tracing output.

marcospassos

marcospassos commented on Dec 22, 2022

@marcospassos
Collaborator

oh that's a good point. Generate for some sample tests from the java "truth" and then anytime somebody changes Target we compare to that?

Exactly. It would help to ensure all targets work as expected. We can even add these trace files to the grammar repository as a ground truth.

marcospassos

marcospassos commented on Dec 22, 2022

@marcospassos
Collaborator

Providing a built-in target-independent check would be amazing. However, running it on the PHP repository would allow for faster feedback and error prevention.

parrt

parrt commented on Dec 22, 2022

@parrt
Member

Sounds good. Let me think about this more. I'd like to get more find grand trace information out first and then we can figure out what sort of testing to do. There are about 350 runtime tests. I wonder if it makes sense to simply always compare the output of the trace with the known good output. It makes some sense and they trace output would not be that big.

parrt

parrt commented on Dec 22, 2022

@parrt
Member

oh right. can't capture stdout unless we run in single-threaded mode...all tracing sends to stdout.

kaby76

kaby76 commented on Dec 23, 2022

@kaby76

I added some code to the trgen template driver generator with a switch for ATN tracing. But it's just for C# and PHP right now. Unfortunately, many of the v4 grammars just aren't PHP compatible because the grammars contain common names between lexer and parser rules (e.g., "COMMENT"/"comment"; see this and this). I plan to add the switch to the other templates today (Cpp, Dart, Go, Java, JavaScript, Python3). Eventually, I'll try to test all v4 grammars with ATN trace diffing. It can't be done in the CI builds because it'll take many hours of CPU time, but at least I can run it once in a while.

Dealing with large trace files can become quite cumbersome. MSYS diff craps out on 1.5GB files, giving bad results. GNU Emacs definitely helps because it can load large files, and offers a "compare-windows" command. Ironically, for the html grammar with abc.com.html, the traces differ pretty much in the beginning of the 1.5GB files, at line 733. (Note, PHP doesn't stop parsing, so I have to kill the process when it reaches 1.5GB.)

A problem with the trace in PHP is that I added a print("\n") to the code. That's not an OS-dependent "newline". So, I have to use dos2unix to standardize the files for comparison. I'm not sure what the call is in PHP to just write a newline customary for the OS.

If a full run of the parser isn't needed to find ATN trace differences, then I might use trwdog, head, tail, split to create a window on the output that I can compare. I could then work in chunks of ~10MB.

Ideally, it would be best if there was some way of stepping through two parsers side-by-size, "one rule at a time" or some standardized step increment, so I can compare two targets side-by-side and not have to run to completion the parse of each target in order to find the first ATN trace difference. I think it's possible with a program that spawns a command-line debugger for each target.

I really don't know the cause of the memory-overflow condition in PHP. I had to start somewhere and decided to just start with ATN trace diffing. My philosophy is to do white-box testing of the internals. I don't think it's enough to test for identical parse trees between targets.

kaby76

kaby76 commented on Dec 23, 2022

@kaby76

Looks like ATN tracing is not available for Cpp and Dart.

marcospassos

marcospassos commented on Dec 23, 2022

@marcospassos
Collaborator

A problem with the trace in PHP is that I added a print("\n") to the code. That's not an OS-dependent "newline". So, I have to use dos2unix to standardize the files for comparison. I'm not sure what the call is in PHP to just write a newline customary for the OS.

@kaby76, just use echo \PHP_EOL;

parrt

parrt commented on Dec 23, 2022

@parrt
Member
parrt

parrt commented on Dec 23, 2022

@parrt
Member

Oh yeah, maybe not dart, but definitely C++. The issue with C++ is that it's a macro TRACE_ATN_SIM that must be set during the runtime build. Here is the argument to the build file. It's annoying that it is not changeable at runtime but we have no choice since it's set up as a macro and C++ people are obsessed with speed.

IF(TRACE_ATN)
    ADD_DEFINITIONS(-DTRACE_ATN_SIM=1)
ENDIF(TRACE_ATN)

I think this means that I manually run a build with cmake TRACE_ATN=ON or whatever then run traceatn.sh.

mpdude

mpdude commented on Oct 12, 2024

@mpdude

I am currently looking into ANTLR to research how it could replace a larger, hand-written parser in the Doctrine ORM project, basically Java's Hibernate in the PHP ecosystem.

The above reads as if you put tons of work into fixing bugs in the PHP runtime. May I ask whether all of these fixes found their way into the runtime already, and this issue here is open only because of a particular problem with some special recursion edge case? Or did all the efforts stall at some point and all of those mentioned bugs are still sitting there?

marcospassos

marcospassos commented on Oct 12, 2024

@marcospassos
Collaborator

Hi @mpdude, these are definitely edge cases I haven't had time to address yet, but I plan to. We've been using this runtime in production for mission-critical applications for years, and it's absolutely production-ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @parrt@marcospassos@mpdude@npgeorgiou-zz@kaby76

        Issue actions

          Allowed memory size of 134217728 bytes exhausted · Issue #12 · antlr/antlr-php-runtime