0

Well, I've been hammering away on this for about a week now with no practical progress because I can't find mental fluidity with the concepts I'm trying to wrangle. I'm Officially Stumped, and would humbly appreciate any insight.

Some minimal background: for several years I've used my own small PHP (CLI-only) introspection helper that provides a d() alternative to print_r() or var_dump(). It features type colorization (using xterm's 256 color palette), and argument literalization (d($a) might show $a: "hi"), and among other nice features, a (much) more compact output layout.

My problem

My question is to do with how the layout system decides how to present nested items. If I have input like

$a = [
  "abcde", "fghij", "klmno", "pqrst"
];

d($a);

I'll get something back like

  (test:3) $a: ["abcde", "fghij", "klmno", "pqrst"]

but if I were to add a small array in the middle that would cause the output to be too long for one line, the output might change to

  (test:3) $a: [
    "abcde",
    "fghij",
    [1, 2, 3, 4, 5],
    "klmno",
    "pqrst"
  ]

This is really nice and seriously tidies up what PHP would output intermixed with a *very* large serving of line breaks.

In the example above you might notice that the outer array expands into multiline mode because it would (rightly) be too long for the line, while the inner array stays in compact mode because it (also rightly) will fit just fine.

I'm not sure if the way I used to do this was arguably terrible or arguably viable: every single dump_* function would speculatively re-execute itself in an assumed non-multiline mode, with the magic $len_test switching my internal output-buffering system into length-measurement mode. Then the magic $len value would contain the amount of data output, and I could straightforwardly check whether it was too long.

Like this:

function dump_string(...) {

  if (!self::$len_test) {     // length test?
    self::$len_test = true;   // if not, enable
    self::dump_string(...);   // and re-execute ourselves
    $multiline = (self::$len > LINE_LIMIT);
    self::$len_test = false;  // this is the outer scope; go back to
                              // text-buffering mode now
  } else {
    $multiline = false;       // test length assuming no newlines etc
                              // (this is the recursive/inner scope)
  }

  ...

}

This method had two problems.

Firstly, not only did properties, getter functions, etc get accessed twice, objects were inconsistently double-instantiated due to the inner function being recursed into while references were held by the outer context. (To add some fun entropy, this would only happen up to the point the function realized it had emitted too much output and bailed out.) This caused spl_object_id() to return generally confusing and innacurate results.

Secondly, the function recursion caused the reference recursion detection elsewhere in my code to become unmanageably hairy, because it basically had to do recursion detection across its own recursion boundaries.

I've worked out a solution for both problems that moves the "is this too long" logic into the output layer and allows the dump_* functions to access each value/getter/property precisely once... but it introduces a huge issue of its own - it limits the is-this-block-too-long analysis to discrete blocks in a way that does not take into account the existing contents of a given line, particularly the (sometimes) large amounts of padding that lines might have in them.

Given the following example,

  (test:3) $a: [
    [
      [
        [
          [
            ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']
          ]
        ]
      ]
    ]
  ]

my new approach only considers the length of the 'aaaa...', disregarding the padding at the start of the line. That line should definitely be wrapping, but as far as the new logic is concerned it doesn't need to because it's blind to the padding.

This is what I'm Officially Stumped on.

My partial solution

You may feel like tacking a crack at reasoning through the problem space before reading on.

To cut a long story short, over about 4 days of very slow mental visualization progress (I had absolutely no idea how to visualize any of this on the computer) I came up with the following.

I have an out() function that accepts text, a stack depth, and a display mode (-1 = always print, 0 = only print if not in multiline mode, 1 = only multiline). I feed this function all the text I want presented for both single- and multiline modes. This function appends to a display list like this:

[
  [ 0, -1, "  (test:3) $a: [" ], # always print           | 16  0  (lengths,
  [ 0,  0, " "                ], # only single-line       | -      explained
  [ 0,  1, "\n    0:"         ], # only multiline         | 23  0  below)
  [ 1, -1, "'abc'"            ], # always print (level 1) | 28  5
  [ 0,  0, " "                ], # only single-line       | -
  [ 0,  1, "\n  "             ], # multiline              | 31
  [ 0, -1, "]"                ], # always                 | 32
]

If only the -1 and 0 parts of the above were printed, you'd get

  (test:3) $a: [ 'abc' ]

while if only the -1 and 1 parts were printed, you'd get

  (test:3) $a: [
    'abc'
  ]

Next, I iterate through the display list, keeping a running total of text length for single-line (0) and output-anyway (-1) modes, per stack depth, and letting the computed length cascade up the stack depth chain. So in the example above, the length at depth 0 is 27 but is counted as 32 because it contains depth 1, which has length 5. This allows me to straightforwardly examine the text lengths per stack depth, and set a multiline flag for the outermost stack depth that is over the limit. Going back to the example above with the nested arrays, this causes the outer array to go multiline while the inner one does not. Win.

The final piece of this puzzle is that text is only emitted in multiline mode or if I set a force-flush flag (which I use just before my dump() routine exits). Whenever out() is called with a depth argument that has multiline mode set, that text is output immediately. This allows the display list to have a very small buffer: it only buffers a few KB of data at most, ever, which was a critical requirement.

Unfortunately, I see no viable way within this model to factor the padding into the length calculations. I also have no idea what besides this model would also work.

The way I reason about this model, it doesn't differentiate between

[[["abcde"]]]
[
  [
    [
      "abcde"
    ]
  ]
]

and

[ \n [ \n [ \n abcde \n ] \n ] \n ]

it's just looking at the stack depths and then enabling some text which happens to be padding as a side-effect of the text within a particular scope being too long.

(Note that I add the padding with mode 1, which is deliberately not considered within the length computations as it would of course always be considered, for all stack depths.)

As might be discerned from the way I've written about this, this type of logic is very alien and unfamiliar to me; I don't really know what I'm doing. If there's anything I can clarify, please do let me know. And if there are any resources I can chew on to (hopefully) provoke my brain to grow/develop in ways that would be helpful to understand this kind of thing better, I would love to hear about them!

1

I would simply produce multiline output into one string variable, and then strip whitespace from the multiline output and put the result into another string variable. If the stripped output is greater than your maximum line length, out() returns the multiline output. Otherwise, it returns the stripped output.

| improve this answer | |
  • Thanks very much for your answer. If I'm understanding you correctly, this approach assumes a global/single-depth, binary all-multiline/all-stripped output model, and also potentially assumes that all output will be buffered until presentation time. With the nested array near the top of my question, the outer one is multiline, the inner one is not; I'm not sure how I might achieve that with this approach. Also, my out() function operates as a streaming buffer - it stores a few bytes/KB of text at any given time at most, flushing as early as it can. – i336_ Jan 26 at 16:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.