. "$BST_LIB" # BST-TIMEOUT: 1800 # TWO python3 starts, and they are the bulk of the cost. # # Everything else here reads the filesystem or dpkg with # # shell builtins, or greps the probe's output. # # # # The two are: a bare `import' (one case, so an import # # failure is legible without reading a transcript) and the # # probe, which takes ONE start for a hundred-odd # # measurements. # # # # THE PROBE IS THE EXPENSIVE ONE, and not because it is # # long: it PARSES. Every parse drives the ATN simulator, # # and the FIRST parse in a process also DESERIALIZES the # # generated parser's ATN and builds the DFA cache from # # cold. That is the single heaviest thing this package # # does, it is pure interpreted python, and its cost on # # wasm32 is not measured here -- which is why the budget is # # 1800 rather than the 1200 a comparable pure-python probe # # takes. A suite-timeout red is a batch-wide cost, so the # # number is deliberately generous. # # # # NOTHING HERE sleeps, opens a socket, starts a thread, # # forks a worker or reaches a device, a peer or a terminal. # # Every parser input is a string literal inside the probe. # # The suite creates one directory under $TMPDIR for the # # probe's transcript and removes it again. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed # # replaces this note in the test plan rather than in this # # comment, which would cost an image build to keep the # # shipped suite byte-identical. # # python3-antlr4-runtime 4.13.1 (meta-openembedded meta-python, # recipes-devtools/python/python3-antlr4-runtime_4.13.1.bb, `inherit pypi # python_setuptools_build_meta', PyPI sdist `antlr4-python3-runtime') on the # wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS, AND WHAT THAT MAKES THE SUITE ASSERT. # ============================================================================ # # It is the RUNTIME HALF of ANTLR 4, and only the runtime half. ANTLR is a # parser generator in two pieces: a Java tool that reads a `.g4' grammar and # WRITES a parser, and a per-language runtime that the written parser is # compiled against. This package is the second piece for Python 3 -- the # InputStream, the Lexer and CommonTokenStream, the Parser, the ATN and DFA # simulators, the error listeners and strategies, and the tree walker and # visitor. The generator is not here and is not on this image. # # So the package HAS NO INTERESTING BEHAVIOUR OF ITS OWN. Nothing in it does # anything until a generated parser drives it. A suite that only imported the # modules and asserted their attributes would be asserting that a set of files # unpacked, which the packaging cases below already do more directly, and would # pass unchanged against a runtime whose ATN simulator had stopped working. # # ---------------------------------------------------------------------------- # WHICH IS WHY THE SUITE CARRIES A GENERATED PARSER AS A FIXTURE. # ---------------------------------------------------------------------------- # # `data/Antlr4RtExpr{Lexer,Parser,Listener,Visitor}.py' are ANTLR-GENERATED # PYTHON, committed to the meta-brintos layer beside this suite. They were # produced by `antlr-4.13.1-complete.jar' -- the tool whose version MATCHES # this runtime's, which is what makes the serialized ATN embedded in the # generated parser loadable by it -- from `src/Antlr4RtExpr.g4' in the same # layer, which is kept for provenance and is not shipped. # # THEY ARE COMMITTED RATHER THAN GENERATED ON TARGET because the generator is a # Java tool: there is no JVM on this image and no ANTLR jar, so generating here # is not an option that was rejected, it is one that does not exist. The # committed form also makes the fixture a CONSTANT -- a red is then a change in # the runtime, never a change in what the generator emitted this morning. # # The grammar is a tiny arithmetic language over integers and identifiers, with # multiplication and division binding tighter than addition and subtraction, # parentheses, and identifiers that admit non-ASCII letters. It is small enough # to reason about completely and still exercises every part of the runtime the # ticket names. # # ---------------------------------------------------------------------------- # WHAT THE BEHAVIOURAL CASES ASSERT, AND WHY EACH IS SHAPED AS IT IS. # ---------------------------------------------------------------------------- # # TREE SHAPE, NOT JUST THE ANSWER. Operator precedence and left # associativity are asserted as the SHAPE of the parse tree # (`tree-precedence', `tree-assoc-sub', `tree-assoc-div'), separately from any # evaluation. This matters: `1+2*3' evaluating to 7 is also consistent with a # wrongly-nested tree and a visitor that compensates, whereas the printed # tree `(expr (expr 1) + (expr (expr 2) * (expr 3)))' can only come from the # multiplication having been nested under the addition. Left associativity is # the half that exercises ANTLR's left-recursion elimination, and a # right-nesting `10-3-2' is the classic symptom of that rewrite going wrong. # # THE TOKEN STREAM AGAINST LITERALS, by type NAME rather than by the raw # integer -- the integers renumber whenever the grammar's token order # changes, so pinning them would make the suite fragile against the fixture # rather than sensitive to the runtime. `WS' is `-> skip', so the # `tokens-spaced' case asserts that whitespace never reaches the stream at # all while the surrounding tokens' text is unaffected. # # getText() OVER THE TREE, which concatenates the LEAVES. Because the start # rule is `prog : expr EOF', the EOF terminal is one of those leaves and the # result therefore ENDS IN THE LITERAL TEXT `'. That is the detail a # reader is most likely to call a bug, so it is asserted explicitly rather # than trimmed away. It is also why `gettext-spaced' returns the UNSPACED # form: the skipped whitespace is not a leaf. # # THE VISITOR AND THE LISTENER OVER ONE TREE, which is the ticket's central # case and the one that drives the walker in both directions. The visitor # dispatches top-down on node type; the listener is driven bottom-up by # ParseTreeWalker over an explicit stack. Each is asserted against python's # own arithmetic AND the two are asserted to agree (`agree-*'), because two # walkers that both visited nothing would agree with each other perfectly. # `walk-enter-count' / `walk-exit-count' / `walk-stack-depth' are the # bookkeeping that says the walk actually DESCENDED rather than firing once # at the root and that its pushes and pops balanced. # # ERROR HANDLING AS BEHAVIOUR, NOT AS ABSENCE. This is the distinction the # ticket insists on. A custom ErrorListener is installed and asserted to be # CALLED, with the LINE and COLUMN of the offence and the offending token's # text -- a suite that merely checked "no exception escaped" would pass # against a runtime that had stopped reporting errors at all. Line is 1-based # and column 0-based, and `err2-*' puts the error on a second line so the # column is seen to RESTART rather than run on as a character offset, which a # single-line case cannot distinguish. # # THE TWO STRATEGIES, asserted as the opposite behaviours they are. # BailErrorStrategy raises ParseCancellationException on the FIRST error and # yields no tree (`bail-raises'); the DEFAULT strategy reports the error and # RECOVERS, returning a tree that still covers the input and still evaluates # (`recover-*'). `bail-clean-tree' asserts that the bail strategy parses # VALID input identically, which is what says `bail-raises' measured the # error path rather than a strategy that is simply broken. Recovery is # asserted in BOTH of its shapes -- an INSERTION for the missing `)' of # `(1+2', visible in the tree as `', and a DELETION for the # stray token of `1+2 3' -- because a strategy that could only insert would # pass the first alone. # # INPUTSTREAM OVER NON-ASCII, because THE RUNTIME INDEXES BY CODE POINT, NOT # BY BYTE. Every string in those cases is longer in UTF-8 bytes than in code # points, and the suite asserts both numbers side by side (`is-size' 3 against # `is-utf8-len' 6) so the claim is a comparison rather than a bare constant. # `is-astral-*' uses a code point OUTSIDE the BMP, which a UTF-16 based # implementation would surrogate-pair and report as size 2. The same fact is # then reached THROUGH THE LEXER (`tokens-unicode', `token-pos-unicode'), so # it is a claim about this port rather than about InputStream in isolation. # # ---------------------------------------------------------------------------- # WHERE THE EXPECTATIONS COME FROM. # ---------------------------------------------------------------------------- # # Every expected value below was MEASURED, by running this suite's own probe # against the payload of the .deb this port builds # (python3-antlr4-runtime_4.13.1-r0_wasm32.deb) under CPython 3.12.3 -- the same # 3.12 the image ships. They are not read off upstream documentation and not # predicted from the source. # # What that does NOT make them is on-target measurements: the host run is # x86-64 CPython over the same pure-python payload, and this port's first # wasm32 run is the batch verify. Since the package contains no compiled code # and no platform branch, the two are expected to agree exactly; where they do # not, the difference is the finding and the test plan records it. # # ---------------------------------------------------------------------------- # PURE PYTHON, WHICH IS WHY IT PORTS. # ---------------------------------------------------------------------------- # # This image's python3 is ONE STATIC BINARY with an empty lib-dynload and no # dynamic loader (BRINTOS-1476), so a release that grew a compiled extension # would install into site-packages and die at `import antlr4'. Measured on the # built .deb: 58 .py files, ZERO .so, wheel tag py3-none-any, Root-Is-Purelib # true. `no-so' and the probe's `so-count' assert that negative from both sides # -- over the packaged tree and over the tree actually imported. # # NO PATCH, NO BBAPPEND AND NO SITE ANSWER. There is no configure step, no # compile step and no C, so there is nothing on this arm for an override to # reach. The recipe's declared runtime dependency is `python3-core' and nothing # else -- the sdist's one `Requires-Dist' is `typing ; python_version < "3.5"', # a marker that is false on 3.12 -- so there is no dependency to strip either, # which is what makes this port a recipe-free one. The recipe-scope build is # measured in the test plan's BRINTOS-2298 section, not restated here. P=python3-antlr4-runtime # ============================================================================ # The suite proper. # ============================================================================ # # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not # run". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/antlr4 DI=$SITE/antlr4_python3_runtime-4.13.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-antlr4-runtime.list PROBE=$BST_DATA/python3-antlr4-runtime-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-antlr4.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- payload --- # WHERE THE PACKAGING SAYS THE PAYLOAD LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the runtime misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" # IT IS A PACKAGE WITH SUBPACKAGES, not a flat module set. The five below are # the runtime's whole internal structure and each is imported by a generated # parser at some point, so a release that reorganised them would relocate the # payload while the behavioural cases still passed. check $P/subpkg-atn test -s "$PKG/atn/__init__.py" check $P/subpkg-dfa test -s "$PKG/dfa/__init__.py" check $P/subpkg-error test -s "$PKG/error/__init__.py" check $P/subpkg-tree test -d "$PKG/tree" check $P/subpkg-xpath test -s "$PKG/xpath/__init__.py" # The modules the fixture and the probe name directly. Listed individually # rather than by count, so a red names the file that went missing. for _m in InputStream Lexer Parser CommonTokenStream BufferedTokenStream \ Token Recognizer RuleContext ParserRuleContext PredictionContext \ IntervalSet TokenStreamRewriter FileStream StdinStream Utils; do check $P/module-$_m test -s "$PKG/$_m.py" done check $P/module-atn-deserializer test -s "$PKG/atn/ATNDeserializer.py" check $P/module-atn-parser-sim test -s "$PKG/atn/ParserATNSimulator.py" check $P/module-atn-lexer-sim test -s "$PKG/atn/LexerATNSimulator.py" check $P/module-error-listener test -s "$PKG/error/ErrorListener.py" check $P/module-error-strategy test -s "$PKG/error/ErrorStrategy.py" check $P/module-tree-tree test -s "$PKG/tree/Tree.py" check $P/module-tree-trees test -s "$PKG/tree/Trees.py" # THE FILE COUNT IS PINNED, so a release that added or dropped a module is # caught even though no case above names it. 58 is what this build's .deb # carries. `-name '*.py'' rather than everything, because the .pyc count is a # consequence of this one and is asserted separately below. check_eq $P/py-file-count 58 \ "$(find "$PKG" -name '*.py' 2>/dev/null | wc -l | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # A FEW SIZES ARE PINNED RATHER THAN FLOORED: a truncated or partially written # file passes `test -s' and fails only here. These are the byte counts this # build's .deb carries, and they span the smallest and the largest module so a # systematic truncation cannot hide between them. check_eq $P/size-init 1125 "$(wc -c < "$PKG/__init__.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check_eq $P/size-input 2334 "$(wc -c < "$PKG/InputStream.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check_eq $P/size-parsersim 80825 "$(wc -c < "$PKG/atn/ParserATNSimulator.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # The byte-compiled copies, which are why `import antlr4' on this target is a # read rather than a parse of nearly a megabyte of source. One per .py, in each # directory's own __pycache__. check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-parser test -s "$PKG/__pycache__/Parser.cpython-312.pyc" check $P/pycache-atn test -s "$PKG/atn/__pycache__/ParserATNSimulator.cpython-312.pyc" check_eq $P/pyc-file-count 58 \ "$(find "$PKG" -name '*.pyc' 2>/dev/null | wc -l | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # NO COMPILED EXTENSION SHIPS. On this image a `.so' in site-packages is not a # speed-up, it is an ImportError: python3 here is one static binary with an # empty lib-dynload (BRINTOS-1476). Asserted over the packaged tree; the # probe's `so-count' asserts the same over the tree actually imported. check_eq $P/no-so "" "$(find "$SITE" -name 'antlr4*.so' 2>/dev/null)" # BST-REDIRECT-OK: forked -- inside $( ) # ------------------------------------------------------------------- bin --- # THE PACKAGE DOES SHIP AN EXECUTABLE, and it is worth asserting because the # ticket describes the package as "a library only" -- which is true of what a # generated parser uses, and not true of what the wheel installs. The sdist # declares a `console_scripts' entry point `pygrun', so a 217-byte launcher # lands in /usr/bin. Asserted as a presence, so the two readings cannot drift. check $P/pygrun-present test -s /usr/bin/pygrun check $P/pygrun-executable test -x /usr/bin/pygrun check $P/pygrun-imports-main grep -q '^from antlr4._pygrun import main$' /usr/bin/pygrun check $P/pygrun-module test -s "$PKG/_pygrun.py" # ------------------------------------------------------------- dist-info --- # The installed metadata, which is what `importlib.metadata' answers from and # the only carrier of the version -- the runtime package itself has no # `__version__' (the probe's `has-version-attr' asserts that absence, and it is # a notable one in a package whose ATN format is version-coupled to a generator). check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-metadata test -s "$DI/METADATA" check $P/dist-info-wheel test -s "$DI/WHEEL" # THE WHOLE FILE SET, not just the files this suite happens to name. A release # that started shipping an extra metadata file would otherwise go unnoticed. # LC_ALL=C so the order is the byte order rather than the guest's locale. check_eq $P/dist-info-file-set \ "METADATA,RECORD,WHEEL,entry_points.txt,top_level.txt," \ "$(LC_ALL=C ls -1 "$DI" 2>/dev/null | LC_ALL=C sort | tr '\n' ',')" # BST-REDIRECT-OK: forked -- inside $( ) # THE DISTRIBUTION NAME AND THE IMPORT NAME DIFFER, which is the thing about # this package a reader is most likely to get wrong: the PyPI distribution is # `antlr4-python3-runtime' and the import is `antlr4'. Both halves asserted -- # the dist-info directory's name above, and top_level.txt here. check_eq $P/dist-info-top-level "antlr4" \ "$(cat "$DI/top_level.txt" 2>/dev/null | tr -d ' \r\n')" # BST-REDIRECT-OK: forked -- inside $( ) check $P/metadata-name grep -q '^Name: antlr4-python3-runtime$' "$DI/METADATA" check $P/metadata-version grep -q '^Version: 4\.13\.1$' "$DI/METADATA" check $P/metadata-license grep -q '^License: BSD$' "$DI/METADATA" # THE ONE DECLARED DEPENDENCY IS DEAD ON THIS IMAGE, and that is why the recipe # needs no RDEPENDS beyond python3-core. The marker `python_version < "3.5"' is # false on 3.12, so `typing' is never required. Asserted as data, because the # recipe's dependency list is a consequence of it. check $P/metadata-requires-typing-marker \ grep -q '^Requires-Dist: typing ; python_version < "3.5"$' "$DI/METADATA" # PURE-PYTHON WHEEL TAGS. `Root-Is-Purelib: true' and the `py3-none-any' tag # are the packaging-side statement of the same fact `no-so' asserts over the # payload. check $P/wheel-purelib grep -q '^Root-Is-Purelib: true$' "$DI/WHEEL" check $P/wheel-tag grep -q '^Tag: py3-none-any$' "$DI/WHEEL" # The console-script entry point, which is where /usr/bin/pygrun comes from. check $P/entry-point-group grep -q '^\[console_scripts\]$' "$DI/entry_points.txt" check $P/entry-point-line grep -q '^pygrun = antlr4._pygrun:main$' "$DI/entry_points.txt" # --------------------------------------------------------------- dpkg --- # The package is REGISTERED, not merely unpacked. A payload that arrived some # other way would pass every path case above and fail here. check $P/dpkg-list test -s "$PKGLIST" check $P/dpkg-lists-init grep -q "^$PKG/__init__\.py\$" "$PKGLIST" check $P/dpkg-lists-pygrun grep -q '^/usr/bin/pygrun$' "$PKGLIST" # ------------------------------------------------------------------ import --- # ONE case for the bare import, so an import failure is legible from the # console without reading the probe's transcript. check $P/import python3 -c 'import antlr4' # ------------------------------------------------------------------ fixture --- # The generated parser this suite drives. Asserted as files BEFORE the probe # runs, so a fixture that failed to install is reported as that rather than as # a hundred behavioural reds. for _f in Antlr4RtExprLexer Antlr4RtExprParser Antlr4RtExprListener Antlr4RtExprVisitor; do check $P/fixture-$_f test -s "$BST_DATA/$_f.py" done # ------------------------------------------------------------------- probe --- if [ ! -s "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" exit 0 fi bst_ok $P/probe-present # 1500s, inside the 1800s suite budget so a hung probe is reported as a probe # overrun naming the last line it printed, rather than as a bare suite timeout. # # The probe EXITS 0 even when the import fails, so this case passing means "the # probe ran", not "the runtime works" -- `import-ok' below is what carries that. bst_probe $P/probe-run "$W/probe" 1500 python3 "$PROBE" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' # because a key is emitted once and a duplicate would otherwise concatenate; # `cut -f2-' because a value may legitimately contain `='. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The probe reached its end. Every case below reads a value it printed, so a # probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" _imp=$(pv import_ok) _fix=$(pv fixture_ok) check_eq $P/import-ok "yes" "$_imp" check_eq $P/fixture-ok "yes" "$_fix" if [ "$_imp" != "yes" ] || [ "$_fix" != "yes" ]; then # Either the runtime did not import or the generated fixture did not. Skip -- # not red -- every behavioural case, each with the reason attached: they are # all one fact, and a hundred reds would bury it rather than report it. # `import', `import-ok' and `fixture-ok' above are the reds that carry the # verdict, and the design doc attributes them. if [ "$_imp" != "yes" ]; then _why="antlr4 did not import: $(pv import_error)" else _why="the generated fixture did not import: $(pv fixture_error)" fi for _c in module-file module-is-package has-version-attr \ has-InputStream has-FileStream has-StdinStream \ has-CommonTokenStream has-Lexer has-Parser has-ParserRuleContext \ has-Token has-RuleContext has-TerminalNode has-ErrorNode \ has-ParseTreeListener has-ParseTreeVisitor has-ParseTreeWalker \ has-RecognitionException has-IllegalStateException has-DFA \ has-PredictionMode has-BailErrorStrategy has-TokenStreamRewriter \ so-count py-file-count-imported \ tree-precedence tree-precedence-rev tree-parens \ tree-assoc-sub tree-assoc-div \ tokens-simple tokens-spaced tokens-parens token-count-simple \ token-pos-first token-pos-third token-pos-multiline \ gettext-simple gettext-spaced gettext-parens \ visit-simple listen-simple agree-simple \ visit-precedence listen-precedence agree-precedence \ visit-assoc listen-assoc agree-assoc \ visit-divassoc listen-divassoc agree-divassoc \ visit-parens listen-parens agree-parens \ visit-nested listen-nested agree-nested \ visit-vars listen-vars agree-vars \ visit-unicode-vars listen-unicode-vars agree-unicode-vars \ walk-enter-count walk-exit-count walk-terminal-count \ walk-stack-depth \ err-called err-count err-line err-column err-offending-text \ err-msg-head err-parser-count err2-line err2-column \ err-listener-count \ bail-raises bail-clean-tree \ recover-strategy recover-errors recover-tree-not-none \ recover-tree recover-eval recover-has-missing \ recover-extra-errors recover-extra-eval \ is-size is-utf8-len is-la1 is-la1-is-alpha is-index-start \ is-text-slice is-consume is-mark-seek \ is-astral-size is-astral-la1 is-astral-utf8-len \ tokens-unicode gettext-unicode token-pos-unicode; do bst_skip $P/$_c "$_why" done rm -rf "$W" exit 0 fi # ---------------------------------------------------------- module surface --- check_eq $P/module-file "__init__.py" "$(pv module_file)" check_eq $P/module-is-package "yes" "$(pv module_is_package)" # NO __version__ ON THE PACKAGE. The version lives only in the dist-info, which # `metadata-version' above asserts. Recorded as the absence it is so a reader # looking for one does not conclude the import failed. check_eq $P/has-version-attr "no" "$(pv has_version_attr)" # The public names a generated parser is written against. Every one of these is # imported by the fixture or by the probe, so these cases and `fixture-ok' # corroborate each other. for _n in InputStream FileStream StdinStream CommonTokenStream Lexer Parser \ ParserRuleContext Token RuleContext TerminalNode ErrorNode \ ParseTreeListener ParseTreeVisitor ParseTreeWalker \ RecognitionException IllegalStateException DFA PredictionMode \ BailErrorStrategy; do check_eq $P/has-$_n "yes" "$(pv has_$_n)" done # TokenStreamRewriter SHIPS AS A MODULE BUT IS NOT RE-EXPORTED from the # package's __init__, so `antlr4.TokenStreamRewriter' does not resolve and a # caller must import the module by path. Asserted as the absence it is -- # `module-TokenStreamRewriter' above asserts the file is there, and the two # together are the whole of the fact. check_eq $P/has-TokenStreamRewriter "no" "$(pv has_TokenStreamRewriter)" # The same two payload facts, over the tree actually imported rather than the # tree the .deb laid down. check_eq $P/so-count "0" "$(pv so_count)" check_eq $P/py-file-count-imported "58" "$(pv py_file_count)" # ------------------------------------------------------------- tree shape --- # PRECEDENCE IS STRUCTURAL. The multiplication nests UNDER the addition in the # first, and the addition over the multiplication in the second -- the two # together say the precedence is a property of the grammar's rule order rather # than of the operand order. check_eq $P/tree-precedence \ "(prog (expr (expr 1) + (expr (expr 2) * (expr 3))) )" \ "$(pv tree_precedence)" check_eq $P/tree-precedence-rev \ "(prog (expr (expr (expr 1) * (expr 2)) + (expr 3)) )" \ "$(pv tree_precedence_rev)" # Parentheses override it, and the parens themselves survive as leaves. check_eq $P/tree-parens \ "(prog (expr (expr ( (expr (expr 1) + (expr 2)) )) * (expr 3)) )" \ "$(pv tree_parens)" # LEFT ASSOCIATIVITY, likewise structural: `10-3-2' nests as `(10-3)-2'. This # is what exercises ANTLR's left-recursion elimination, and a right-nesting # tree here is that rewrite having gone wrong. check_eq $P/tree-assoc-sub \ "(prog (expr (expr (expr 10) - (expr 3)) - (expr 2)) )" \ "$(pv tree_assoc_sub)" check_eq $P/tree-assoc-div \ "(prog (expr (expr (expr 100) / (expr 10)) / (expr 2)) )" \ "$(pv tree_assoc_div)" # ------------------------------------------------------------ token stream --- check_eq $P/tokens-simple "INT:1 ADD:+ INT:2 MUL:* INT:3 EOF:" \ "$(pv tokens_simple)" # WS IS SKIPPED, so no whitespace token reaches the stream and the surrounding # tokens' text is unchanged. Both halves are in this one expectation. check_eq $P/tokens-spaced "INT:1 ADD:+ INT:2 EOF:" "$(pv tokens_spaced)" check_eq $P/tokens-parens "LPAREN:( INT:1 ADD:+ INT:2 RPAREN:) MUL:* INT:3 EOF:" \ "$(pv tokens_parens)" # The EOF token is counted: 5 real tokens plus it. check_eq $P/token-count-simple "6" "$(pv token_count_simple)" # TOKEN POSITIONS as line:column:start:stop. Line is 1-based, column is # 0-based, and start/stop are absolute CODE POINT offsets into the input. check_eq $P/token-pos-first "1:0:0:0" "$(pv token_pos_first)" check_eq $P/token-pos-third "1:2:2:2" "$(pv token_pos_third)" # On a second line the line advances and the column RESTARTS at 0, while # start/stop keep running -- the distinction a single-line case cannot make. check_eq $P/token-pos-multiline "2:0:3:3" "$(pv token_pos_multiline)" # ---------------------------------------------------------------- getText --- # getText() CONCATENATES THE LEAVES, and because `prog : expr EOF' the EOF # terminal is one of them -- hence the trailing literal `'. Asserted # rather than trimmed, because it is the detail most likely to be read as a bug. check_eq $P/gettext-simple "1+2*3" "$(pv gettext_simple)" # The skipped whitespace is not a leaf, so the spaced input round-trips to the # UNSPACED form. Same tree as `gettext-simple', reached from different text. check_eq $P/gettext-spaced "1+2*3" "$(pv gettext_spaced)" check_eq $P/gettext-parens "(1+2)*3" "$(pv gettext_parens)" # ------------------------------------------------- visitor and listener --- # Each walker against python's own arithmetic, and then the two against each # other. The third case is not redundant: two walkers that both visited nothing # would agree with each other perfectly, so agreement is only evidence # alongside the absolute values. check_eq $P/visit-simple "7" "$(pv visit_simple)" check_eq $P/listen-simple "7" "$(pv listen_simple)" check_eq $P/agree-simple "yes" "$(pv agree_simple)" check_eq $P/visit-precedence "26" "$(pv visit_precedence)" check_eq $P/listen-precedence "26" "$(pv listen_precedence)" check_eq $P/agree-precedence "yes" "$(pv agree_precedence)" # 10-3-2 is 5 left-associated and 9 right-associated, so this is the # EVALUATED counterpart of `tree-assoc-sub'. check_eq $P/visit-assoc "5" "$(pv visit_assoc)" check_eq $P/listen-assoc "5" "$(pv listen_assoc)" check_eq $P/agree-assoc "yes" "$(pv agree_assoc)" # 100/10/2 is 5 left-associated and 20 right-associated. check_eq $P/visit-divassoc "5" "$(pv visit_divassoc)" check_eq $P/listen-divassoc "5" "$(pv listen_divassoc)" check_eq $P/agree-divassoc "yes" "$(pv agree_divassoc)" check_eq $P/visit-parens "9" "$(pv visit_parens)" check_eq $P/listen-parens "9" "$(pv listen_parens)" check_eq $P/agree-parens "yes" "$(pv agree_parens)" check_eq $P/visit-nested "3" "$(pv visit_nested)" check_eq $P/listen-nested "3" "$(pv listen_nested)" check_eq $P/agree-nested "yes" "$(pv agree_nested)" # Identifiers, which is the Var alternative and a dict lookup in the walker. check_eq $P/visit-vars "42" "$(pv visit_vars)" check_eq $P/listen-vars "42" "$(pv listen_vars)" check_eq $P/agree-vars "yes" "$(pv agree_vars)" # NON-ASCII IDENTIFIERS through the whole pipeline: lexer, parser, both # walkers. This is the code-point claim made as behaviour rather than as a # stream measurement. check_eq $P/visit-unicode-vars "21" "$(pv visit_unicode_vars)" check_eq $P/listen-unicode-vars "21" "$(pv listen_unicode_vars)" check_eq $P/agree-unicode-vars "yes" "$(pv agree_unicode_vars)" # THE WALK ACTUALLY DESCENDED. `1+2*3' is 1 prog + 5 expr = 6 rule nodes, so # enter and exit each fire 6 times; its terminals are 1, +, 2, *, 3 and EOF = # 6. A walker that fired once at the root would still produce the right answer # for a single-node tree and is caught only here. check_eq $P/walk-enter-count "6" "$(pv walk_enter_count)" check_eq $P/walk-exit-count "6" "$(pv walk_exit_count)" check_eq $P/walk-terminal-count "6" "$(pv walk_terminal_count)" # ... and the pushes and pops balanced: exactly the answer is left. check_eq $P/walk-stack-depth "1" "$(pv walk_stack_depth)" # ---------------------------------------------------------- error listener --- # THE LISTENER IS CALLED. Asserted as a positive, with the position and the # offending token, rather than as "nothing was raised". check_eq $P/err-called "yes" "$(pv err_called)" check_eq $P/err-count "1" "$(pv err_count)" check_eq $P/err-line "1" "$(pv err_line)" # Column 2 is the `*' of `1+*2', 0-based. check_eq $P/err-column "2" "$(pv err_column)" check_eq $P/err-offending-text "*" "$(pv err_offending_text)" check_eq $P/err-msg-head "extraneous" "$(pv err_msg_head)" # The parser's own counter agrees with the listener's tally, which is what says # the listener saw every error rather than the first. check_eq $P/err-parser-count "1" "$(pv err_parser_count)" # LINE IS 1-BASED AND COLUMN 0-BASED, and the column RESTARTS on a new line. # With the same error moved to line 2 the column is 0, not a running offset. check_eq $P/err2-line "2" "$(pv err2_line)" check_eq $P/err2-column "0" "$(pv err2_column)" # REMOVING THE DEFAULT LISTENER IS WHAT KEEPS THE CONSOLE CLEAN: the probe # calls removeErrorListeners() before adding its own, so exactly one remains # and a syntax error prints nothing to stderr. check_eq $P/err-listener-count "1" "$(pv err_listener_count)" # --------------------------------------------------------- error strategy --- # BAIL RAISES ON THE FIRST ERROR and yields no tree. check_eq $P/bail-raises "ParseCancellationException" "$(pv bail_raises)" # ... and on VALID input raises nothing and produces the same tree the default # strategy does. Without this, `bail-raises' is equally consistent with a # strategy that is simply broken. check_eq $P/bail-clean-tree \ "(prog (expr (expr 1) + (expr (expr 2) * (expr 3))) )" \ "$(pv bail_clean_tree)" # THE DEFAULT STRATEGY RECOVERS AND CONTINUES. `(1+2' is missing its `)': the # error is reported, a token is INSERTED, and the parse still returns a tree # that covers the input and still evaluates. check_eq $P/recover-strategy "DefaultErrorStrategy" "$(pv recover_strategy)" check_eq $P/recover-errors "1" "$(pv recover_errors)" check_eq $P/recover-tree-not-none "yes" "$(pv recover_tree_not_none)" check_eq $P/recover-tree \ "(prog (expr ( (expr (expr 1) + (expr 2)) ) )" \ "$(pv recover_tree)" check_eq $P/recover-eval "3" "$(pv recover_eval)" # THE RECOVERY IS VISIBLE IN THE TREE rather than silently papered over: the # inserted token carries `' text. check_eq $P/recover-has-missing "yes" "$(pv recover_has_missing)" # THE OTHER SHAPE OF RECOVERY -- a DELETION. `1+2 3' has a stray token, which # is dropped; the parse of `1+2' completes and evaluates to 3. A strategy that # could only insert would pass `recover-*' above and fail here. check_eq $P/recover-extra-errors "1" "$(pv recover_extra_errors)" check_eq $P/recover-extra-eval "3" "$(pv recover_extra_eval)" # ------------------------------------------------------------ InputStream --- # THE RUNTIME INDEXES BY CODE POINT, NOT BY BYTE. "αβγ" is 3 code points and 6 # UTF-8 bytes, and both numbers are asserted side by side so the claim is a # comparison rather than a bare constant: a byte-indexing stream would report # size 6 here. check_eq $P/is-size "3" "$(pv is_size)" check_eq $P/is-utf8-len "6" "$(pv is_utf8_len)" # LA(1) is the CODE POINT, not the first byte -- 945 is U+03B1, where a byte # stream would give 206, the leading byte of its UTF-8 encoding. check_eq $P/is-la1 "945" "$(pv is_la1)" check_eq $P/is-la1-is-alpha "yes" "$(pv is_la1_is_alpha)" check_eq $P/is-index-start "0" "$(pv is_index_start)" # getText takes an INCLUSIVE code-point range, so 0..1 is the first two # characters -- six bytes of output from a two-unit slice. check_eq $P/is-text-slice "αβ" "$(pv is_text_slice)" # consume() advances by ONE CODE POINT: index 1, and LA(1) is now U+03B2. check_eq $P/is-consume "1:946" "$(pv is_consume)" # seek() is likewise in code points, and rewinding restores the first. check_eq $P/is-mark-seek "0:945" "$(pv is_mark_seek)" # A CODE POINT OUTSIDE THE BMP, which a UTF-16 based implementation would # surrogate-pair and report as size 2. Four UTF-8 bytes, one unit, one LA. check_eq $P/is-astral-size "1" "$(pv is_astral_size)" check_eq $P/is-astral-la1 "120793" "$(pv is_astral_la1)" check_eq $P/is-astral-utf8-len "4" "$(pv is_astral_utf8_len)" # THE SAME FACT REACHED THROUGH THE LEXER, so it is a claim about this port # rather than about InputStream in isolation: non-ASCII identifiers are single # ID tokens and their text round-trips through the tree. check_eq $P/tokens-unicode "ID:π ADD:+ ID:ф EOF:" "$(pv tokens_unicode)" check_eq $P/gettext-unicode "π*ф" "$(pv gettext_unicode)" # The `+' after a two-byte alpha is at COLUMN 1, not column 2: the column # counts code points. check_eq $P/token-pos-unicode "1:1:1:1" "$(pv token_pos_unicode)" rm -rf "$W" exit 0