Skip to content

v0.1.1

Latest

Choose a tag to compare

@github-actions github-actions released this 31 Jul 05:10
· 5 commits to main since this release
v0.1.1
6f472d7

Changed

  • t/runtime-builtins-io-objects-php85-test.lisp grew back over the
    500-line limit (511 lines) as later work in this session's own
    history added more tests to it. Its six Dom\Element/
    Dom\ParentNode/Dom\HTMLDocument tests split out to a new
    t/runtime-builtins-io-dom-php85-test.lisp — the same size-driven,
    topic-scoped split already applied once before in this file's history
    to pull out runtime-builtins-io-uri-php85-test.lisp. A fresh sweep
    of every file's current line count (wc -l src/*.lisp t/*.lisp,
    since several files grew substantially across this session's edits)
    found this as the only file over the limit; no src/ file is over
    500 lines (runtime-builtins-core.lisp sits exactly at it).
  • Every sbcl --script invocation in flake.nix (checks.default,
    packages.coverage, and apps.test/nix run .#test) now runs under
    timeout (600s for the plain test suite and the interactive nix run
    entry point, 900s for coverage's slower force-recompile-then-run), so a
    hang — an infinite loop, a VM/fiber deadlock — fails the build loudly
    instead of running until CI's own outer timeout eventually kills it.
    These were the only unguarded command invocations in the repository: the
    GitHub Actions workflows already carry timeout-minutes on every job, and
    there is no other place this codebase shells out to a subprocess.
  • json_validate (%php-json-validate) now delegates to
    cl-json-kit's
    json-kit:parse, a dependency-free, RFC 8259-conformance-tested JSON
    reader (the full JSONTestSuite corpus: 95/95 must-accept, 188/188
    must-reject), instead of a hand-rolled strict parser added earlier in this
    same series of changes. That hand-rolled version already regression-tested
    clean, but a purpose-built, conformance-tested library is strictly more
    correct with less code to maintain — e.g. it correctly rejects a leading
    zero before more digits ("01"), a real RFC 8259 rule the hand-rolled
    digit-scanner missed. This is cl-cc-php's first adoption of a
    nerima-lisp sibling package as a genuine runtime dependency (cl-weave,
    cl-prolog, and cl-parser-kit are test-only): it is now a :depends-on
    of the shipped cl-cc-php system, not just cl-cc-php/test, wired into
    flake.nix/run-tests.lisp/coverage.lisp the same way the cl-cc
    subsystems are, and documented in docs/src/installation.md's dependency
    table. %php-json-decode/%php-json-encode deliberately still use the
    original hand-rolled reader/writer: PHP's json_decode needs values
    translated into this runtime's own %php-array/+php-null+/PHP-boolean
    representation, which is a real, non-trivial mapping — not the "weird
    unnecessary adapter" this project avoids — and migrating it is a larger,
    separately-scoped change than json_validate's zero-value-model swap.

Fixed

  • final class Foo { ... } had the exact same gap abstract class
    did (see below): no registered top-level statement parser of its
    own, so it fell through to generic expression-statement parsing and
    errored on the bare final keyword. Found by re-checking the
    abstract-class fix's own investigation notes, which had already
    identified final as sharing the same gap without fixing it at the
    time. Fixed the same way, registering a :final statement parser
    mirroring :abstract's. As with abstract, this only makes the
    class parse — nothing tracks "is this class final" past parsing, so
    extending a final class is not rejected either, confirmed by a test
    documenting that current behavior rather than implementing the
    enforcement.
  • An enum using a trait (enum S { use Greetable; case A; }) failed
    outright when a merged-in method was actually called. Found
    immediately after the class-trait fix above by applying the same
    "does this related feature have the same class of gap" check to
    enums — t/parser-class-test.lisp's
    php-parser-enum-implements-methods-traits-and-constants test
    combines enum ... { use HasLabels; ... } but, like the class-trait
    tests before the fix above, only checks the parser's raw AST shape.
    Two separate, non-obvious causes, found by reading the actual error
    at each step rather than assuming the first fix covered enums too:
    (1) %php-merge-all-trait-members's original (dolist (stmt stmts) (when (ast-defclass-p stmt) ...)) walk never found an enum's
    ast-defclass at all — %php-parse-classlike wraps an enum's class
    definition in an ast-progn alongside a %php-enum-finalize call,
    unlike a plain class, whose ast-defclass is a bare top-level form —
    so the merge silently never ran for any enum. Fixed by also checking
    one level into an ast-progn's forms. (2) Once the merge did run, a
    merged-in method still failed: enum methods dispatch through the
    shared enum class object and need :allocation :class, not the
    ordinary per-instance :allocation :instance a trait method carries
    from where it was declared — %php-parse-classlike already applies
    this fixup to methods declared directly in an enum body, but a method
    merged in later, by a separate pass, never went through that code
    path. Fixed by applying the same fixup to merged-in methods when the
    target is an enum.
  • abstract class Foo { abstract function m(): T; ... } failed to parse
    at all — PHP parse error: unexpected keyword :ABSTRACT in expression. :readonly has its own top-level statement parser
    (readonly class Foo {...}, src/parser-class.lisp) that requires
    class to immediately follow and delegates to
    %php-parse-class-decl; :abstract (and :final) had no such
    registration, so a leading abstract before class fell through to
    generic expression-statement parsing and errored on the bare keyword.
    Member-level abstract function ...; inside a class/trait/interface
    body already worked (%php-parse-visibility-modifiers already
    recognized :abstract there) — only the class-level modifier itself
    was missing. Found while investigating whether abstract classes work
    at all (no test anywhere exercised the class-level abstract
    modifier before this). Fixed by registering a :abstract statement
    parser mirroring :readonly's exact pattern. Not fixed, and
    confirmed as a separate, real gap by the same investigation:

    instantiating an abstract class directly (new Shape() where Shape
    is abstract) is not rejected — nothing currently tracks "is this
    class abstract" as metadata past parsing, and no instantiation-time
    check exists to consult it, unlike real PHP's fatal "Cannot
    instantiate abstract class" error. Documented via a test asserting
    the current (unenforced) behavior rather than implemented, given the
    additional scope (tracking abstractness through to whatever lowers
    new) this investigation did not cover.
  • PHP trait methods did not actually work at all. The simplest
    possible case — trait Greetable { function greet() { return 'hi'; } } class C { use Greetable; } (new C())->greet() — failed outright
    with The slot CL-CC/PHP::GREET is missing from the object of class CL-CC/PHP::C. Every trait test before this fix either checked the
    parser's raw AST shape directly (t/parser-trait-test.lisp, which
    never compiled or ran anything) or used empty traits and checked
    only class_uses() reflection (which did work — trait names were
    always tracked correctly). None ever called an actual trait method,
    which is the entire point of a trait. Root cause: use TraitA;
    inside a class body produced only an internal :PHP-TRAIT-USE marker
    slot-def (%php-parse-use-trait-stmt, src/parser-trait.lisp) that
    nothing in class lowering ever consulted — confirmed with grep,
    there was no reference to :php-trait-use/:php-trait-names
    anywhere outside parser-trait.lisp, and *php-trait-registry*
    (trait name → member list, populated by trait declarations) had no
    reader at all. Fixed with a new whole-program pass,
    %php-merge-all-trait-members (run from parse-php-source, after
    every top-level form — including every trait declaration — is
    parsed, so a class using a trait declared later in the same file
    still resolves correctly): it replaces each class's trait-use marker
    with the real member slot-defs of the traits it names. A member name
    defined by only one used trait is copied in directly. A member name
    defined by more than one used trait is resolved by a matching
    insteadof clause (TraitA::member insteadof TraitB; keeps TraitA's
    version); a collision insteadof does not resolve now signals a
    clear error — matching real PHP's fatal "has not been applied,
    because there are collisions" — instead of silently picking whichever
    trait happened to be listed first, which is what would have happened
    with no conflict handling at all. t/parser-trait-test.lisp's ten
    parser-level tests (which check that use ... { insteadof/as }
    syntax parses into the correct raw metadata) needed updating to read
    that metadata from *php-trait-applications* instead of the now-
    merged-away marker slot-def — their original intent (verify parsing
    captured the right insteadof/alias data) is unchanged, only the
    channel they read it through. as aliasing is also implemented, by
    %php-apply-trait-aliases: TraitA::method as alias; adds a renamed
    copy of the resolved member under the alias name — purely additive,
    the original name stays callable too, matching real PHP — and
    method as protected; (visibility only, no rename) replaces the
    merged member's :php-modifiers visibility keyword in place. Both
    verified end-to-end. (Not verified: whether the changed visibility is
    actually enforced against external callers — that is a separate
    mechanism this investigation did not check.)
  • Prefix ++/-- on a non-assignable operand (anything besides a
    $variable, array element, or property — e.g. ++(1+1)) reported a
    confusing internal SB-INT:SIMPLE-PROGRAM-ERROR "invalid number of arguments: 2" instead of the intended, clear diagnostic:
    php-lower-prefix-incdec's fallback called %php-unsupported (which
    takes exactly one message argument) with an extra, unused second
    argument. Found while designing a smaller, bounded alternative to
    fully implementing goto (see below) — routing goto's missing
    label-statement case through this same existing
    "unsupported-parse-form" mechanism, which meant actually reading how
    every other caller used it first. Fixed by removing the extra
    argument; this was the only call site.
  • http_build_query silently dropped every key/value pair after the
    first: joining used (format nil "~{~A~^~A~}" (list (car (nreverse parts)) sep)), the exact same "reads as a join but only ever consumes
    two list elements" shape as %php-fputcsv's bug earlier in this file
    http_build_query(['a'=>1,'b'=>2,'c'=>3]) returned just "a=1&",
    not "a=1&b=2&c=3". Found while investigating
    runtime-builtins-io.lisp's low coverage (the function had zero
    tests of any kind). Fixed with the same explicit
    part-then-separator-if-more loop %php-fputcsv uses.
  • is_numeric() (%php-is-numeric/%php-string-numeric-p) and
    filter_var(..., FILTER_VALIDATE_FLOAT) (%php-filter-float-value)
    both delegated straight to Common Lisp's read-from-string to decide
    whether a string was numeric, which accepts syntax PHP's own
    numeric-string grammar does not: a ratio like "1/2" reads as the CL
    ratio 1/2 — a numberp — and a double-float exponent marker like
    "1.0d0" reads as a valid CL float literal, so is_numeric("1/2")
    and is_numeric("1.0d0") both incorrectly returned true (real PHP:
    false for both). is_numeric is common enough that this had real
    reach beyond the two functions directly affected. Fixed with a new
    %php-numeric-grammar-p, an explicit character-level scan of PHP's
    actual grammar (optional sign, digits with an optional fractional
    part, optional e/E exponent), shared by both functions — CL's
    reader is still used afterward, but only for the numeric conversion
    in %php-filter-float-value, once the grammar check has already
    confirmed the string can't be one of the CL-only forms that would
    make that conversion produce something PHP wouldn't. Verified against
    17 cases including both counter-examples and PHP's own valid-numeric
    edge cases ("007", "-.5", "5.", surrounding whitespace).
    %php-floatval/floatval() has a separate, deliberately more lenient
    leading-numeric-prefix parser (floatval("1.5abc") is valid PHP,
    returning 1.5, unlike is_numeric/filter_var's whole-string
    requirement) and was not touched — conflating the two would risk
    changing already-tested, working behavior for a different function
    with different semantics.
  • %php-filter-var's FILTER_VALIDATE_* dispatch (src/runtime-builtins- types.lisp) used bare magic numbers (257/258/259/273/274/516)
    in a case even though named constants for every one of them
    (+php-filter-validate-int+ etc.) already existed right above it —
    found while re-verifying the "left in place as incomplete" note under
    Removed below, which turned out to be based on a stale read of this
    function. The actual reason those constants were unreferenced: case
    keys are not evaluated in Common Lisp, so writing
    (case filter ((+php-filter-validate-int+) ...)) would have compared
    filter against the literal symbol, never against 257 — using them
    as case keys was never an option, which is presumably why whoever
    wired the dispatch used the raw numbers instead and left the constants
    dangling. Fixed by rewriting the dispatch as a cond of =
    comparisons against the named constants, which both eliminates four
    more entries from paredit inspect unused-definitions's output and
    makes the dispatch's six filter IDs self-documenting at each call
    site instead of requiring a lookup against the PHP manual's numeric
    FILTER_VALIDATE_* values. Behavior is unchanged (same five integers,
    same handler functions) — confirmed by the full suite passing
    identically before and after.
  • 12 more files still had the stale packages/php/src/... header path
    from before the extraction out of the cl-cc monorepo — the [0.1.0]
    fix below for "seventeen source-file header comments" predates several
    of these files' current form (some are later splits in this same
    session's own work, inheriting the stale header from their pre-split
    parent) and simply never reached the rest. Found with grep -rl '^;;;; packages/' src/*.lisp; every file in src/ now has the
    post-extraction header format.
  • %php-checkdate accepted invalid dates like February 30th or April 31st:
    encode-universal-time alone only range-checks a day-of-month against
    1-31, never against the actual length of the given month, so nothing in
    the old implementation ever caught the difference. Fixed by cross-
    checking against a new %php-days-in-month helper (also now used by
    %php-date's existing leap-year handling, removing one duplicate copy of
    the same Gregorian leap-year rule).
  • %php-fputcsv wrote only the first field of a multi-field row: joining
    the fields used (format nil "~{~A~^~A~}" (list (car parts) sep)), which
    reads as a join but iterates a two-element list — (car parts), then
    sep itself misread as a second field — so every field after the first
    was silently dropped. Found by writing the test this file had never had
    (see below); fixed with an explicit field-then-separator loop.
  • similar_text (%php-similar-text) computed the wrong answer for many
    inputs: it counted, for each character of the first string, whether that
    character occurred anywhere at all in the second string, with no
    position tracking — not PHP's real algorithm (find the longest common
    substring, then recurse on the parts of both strings before and after
    it). The two algorithms happened to agree on PHP's own manual example
    (similar_text('World', 'Word') === 4), which is why it went
    unnoticed, but diverge on simple cases: the old code returned 2, not
    PHP's real 1, for similar_text('ab', 'ba'). Replaced with the
    documented recursive longest-common-substring algorithm
    (%php-similar-text-lcs/%php-similar-text-count), verified against
    both the manual example and the 'ab'/'ba' counter-example. The
    third &$percent by-reference parameter — previously silently
    ignored entirely ((declare (ignore percent-var))) — is now
    implemented and registered in *php-by-ref-param-registry*
    alongside settype, the only other entry that registry has ever had.
    Writing an end-to-end PHP-source test for it (similar_text($a, $b, $p); echo $p;) is incidentally the first test in this repository to
    verify the by-reference-builtin-parameter mechanism itself works
    through the full compile-and-run pipeline, not just at the parser's
    AST-shape level or by calling the Lisp function directly with a
    hand-built ref.
  • %php-compile-regex (the hand-rolled PHP preg_* NFA engine) is now a
    proper backtracking matcher instead of the previous "greedy, never undoes
    a match" one: preg_match('/a*a/', 'aaa') now correctly matches (a*
    gives one character back to the trailing literal a), where the old
    engine committed to consuming all three as for a* with no way to give
    any back. Implemented as continuation-passing style internally — every
    compiled piece takes a success continuation and returns its result, which
    is what lets a quantifier retry the rest of the pattern at a shallower
    position instead of only ever offering its single longest match. Capturing
    groups now correctly undo their recorded span when the continuation
    representing everything after them goes on to fail, rather than keeping a
    stale span from an abandoned branch. The change is entirely internal:
    %php-compile-regex's external (matcher str pos g) -> end-pos-or-nil
    contract, and every one of its callers, is unchanged. Verified against the
    full 1401-test suite (no regressions) plus two new regression tests for
    the a*a-style case and for a capturing group backing off correctly.
  • %php-json-validate (json_validate, PHP 8.3) always returned T,
    for any input, including obviously malformed JSON: it ran
    %php-json-decode inside a handler-case for error, but
    %php-json-decode's own parser never signals — every unrecognised
    token in %php-json-parse-value falls through to a catch-all branch
    that returns null instead of raising, by design, so that
    json_decode can offer PHP's "returns null on malformed input"
    contract. That made the handler-case's error clause structurally
    unreachable underneath json_validate. Fixed by giving
    json_validate its own dedicated strict parser
    (%php-json-strict-value/-string/-number/-object/-array) that
    signals on the first syntax problem instead of defaulting to null, and
    by checking that parsing consumed the entire input, since PHP rejects
    trailing garbage like "123abc". %php-json-decode/json_decode
    itself is unchanged — it still tolerantly returns null on malformed
    input, and still ignores the JSON_THROW_ON_ERROR flag, matching PHP's
    behaviour only when that flag is unset.

Changed

  • src/runtime-builtins-string-json.lisp: the bounds-checked
    (and (< pos (length s)) (char= (char s pos) X)) guard, repeated 17
    times across both JSON parsers, is now the one inlined predicate
    %php-json-at-p. Applied with paredit query replace against the
    literal S-expression shape, not by hand, so every site changed
    identically in one pass.
  • src/grammar.lisp/src/grammar-stmt.lisp: (eq (php-ts-peek-type ts) X) and (eq (php-ts-peek-value ts) X) — 54 and 7 call sites,
    respectively, found via paredit inspect duplicates — are now
    php-ts-at-type-p/php-ts-at-value-p, alongside the file's existing
    php-ts-at-end-p predicate whose naming they follow. Same
    paredit query replace technique as the JSON change above; the sweep
    ran before the two new functions existed, specifically to avoid the
    self-match failure mode that technique has (a generic S-expression
    pattern doesn't know not to rewrite the very definition it's building).

Added

  • Two tests in t/parser-class-e2e-test.lisp covering readonly
    properties, which had zero end-to-end coverage of any kind before
    this — only parser-level tests confirming the modifier is captured as
    AST metadata. Confirms a readonly property can be set once during
    construction, and — the same class of gap as abstract-class
    instantiation elsewhere in this file — confirms that reassigning one
    afterward is currently not rejected, unlike real PHP's fatal
    "Cannot modify readonly property" error. Documented via a test
    asserting the current, unenforced behavior; enforcing it needs the
    property-write lowering to check readonly-ness and the object's
    already-initialized-or-not state, which this investigation did not
    trace.
  • A test in t/parser-class-e2e-test.lisp confirming an abstract class
    using a trait, and a concrete subclass of it, both correctly get the
    trait's methods — the same combination that turned up the enum+trait
    bug above, checked here since it plausibly could have had the same
    kind of gap. It did not: this composition already worked correctly.
  • Two tests in t/parser-class-e2e-test.lisp confirming basic
    interface/abstract-class correctness that nothing had verified
    end-to-end before: an interface constant is actually readable through
    an implementing class (interface HasVersion { const VERSION = '1.0'; } class Impl implements HasVersion {}Impl::VERSION correctly
    returns '1.0'; t/parser-interface-test.lisp's several
    interface-constant tests all only checked the parser's raw AST shape,
    none ever compiled or ran anything), and a concrete subclass of an
    abstract class correctly calls both its own override of an abstract
    method and a concrete method the abstract parent provides. Found
    while investigating whether other foundational PHP OOP constructs had
    the same class of "looks tested, never actually exercised end-to-end"
    gap the trait-methods fix above uncovered — abstract-class parsing
    turned out to have exactly that gap (see Fixed); interfaces did not.
  • A test in t/parser-e2e-test.lisp confirming that goto is currently
    unusable for any real PHP program: goto label; itself parses fine
    (:goto is a registered statement keyword), but the corresponding
    label: target statement was not recognized anywhere in the main
    parser pipeline — php-parse-statement (src/parser-class.lisp)
    dispatched only on :T-KEYWORD tokens, with no check for a bare
    identifier immediately followed by a colon, the syntax a label
    statement uses, so it fell through to the generic expression-statement
    parser and failed with a generic, unhelpful PHP parse error: unexpected token (:TYPE :T-COLON ...). goto/label had zero test
    coverage of any kind before this, found while investigating
    parser-stmt-decls-modules.lisp's low coverage. A correct fix is a
    real feature, not a bounded one — it needs a label-statement AST
    representation and block-level lowering that recognizes labels and
    switches a plain statement block to a Common Lisp TAGBODY (today
    only loops/switches build one internally, for their own generated
    break/continue tags), plus correctly modeling PHP's own
    goto-scoping restrictions (no jumping into a loop/switch/variable
    scope from outside it) — so, matching the precedent elsewhere in this
    file, that full feature is not attempted here, where a rushed fix
    risks a silently-wrong jump target being worse than a clean error.
    What is bounded and safe, and now implemented: php-parse-statement
    recognizes the "identifier immediately followed by colon" shape
    explicitly and reports it through %php-unsupported (src/parser- support.lisp) — the same mechanism every other not-yet-supported
    parse form in this codebase already uses — so the error at least names
    the construct that triggered it instead of a generic token complaint.
  • A test in t/runtime-builtins-io-tokenizer-php85-test.lisp confirming
    token_get_all has no dedicated heredoc/nowdoc (<<<EOT ... EOT)
    handling — none of %php-token-get-all's internal string/comment
    scanners ever mention <<< — but, importantly, degrades gracefully
    rather than crashing or hanging on heredoc-containing source: it still
    returns a well-formed token array, just not one matching real PHP's
    heredoc/nowdoc token shape. A real fix is a genuine new tokenizer
    feature (heredoc/nowdoc's own closing-identifier and PHP 7.3+ flexible
    indentation-stripping rules), not a bounded bug fix, so — matching the
    precedent set by date_modify/Fiber resume/ReflectionProperty/
    array_walk earlier in this file — documented via a test rather than
    attempted here.
  • Two direct tests for %php-session-apply-start-options
    (session_start($options)'s option-merging logic) in
    t/runtime-builtins-io-cookie-session-test.lisp, investigating
    runtime-builtins-io-cookie-session.lisp's low coverage: the
    function had zero tests calling it by name (only reachable indirectly
    through the high-level session_start() e2e tests) despite being the
    single largest function in the file. Confirms it correctly merges
    every cookie_* option, and — the easy way for this kind of
    repetitive option-merging code to go wrong — correctly falls back to
    the existing session cookie param rather than a hard-coded default
    when an option key is omitted. *PHP-SESSION-NAME*/
    *PHP-SESSION-COOKIE-PARAMS* are defvars mutated in place by the
    function under test, so both tests let-bind them to avoid leaking
    session state into other tests.
  • Six sscanf format-specifier cases in
    t/runtime-builtins-register-e2e-test.lisp, investigating
    runtime-builtins-io-scan.lisp's low coverage: the suppress flag
    (%*d), a literal %%, octal (%o), %u correctly rejecting a
    leading - (unlike %d), and the %e/%g float-format aliases had
    no test exercising them at all. Unlike every other coverage-driven
    addition in this file, this one confirmed the existing implementation
    is already correct on every case — traced by hand against
    %php-sscanf-scan-int/-scan-float before writing each assertion,
    and every trace matched the actual test run with no fix needed.
  • Two tests in t/runtime-builtins-array-e2e-test.lisp documenting a real
    array_walk/array_walk_recursive limitation found while investigating
    runtime-builtins-regex-array.lisp's low coverage (both functions had
    zero tests of any kind before this): PHP's real signature for the
    callback is function(&$value, $key) — the value is by reference, so a
    callback that reassigns it is documented to mutate the array in place —
    but %php-array-walk/%php-array-walk-recursive invoke the callback
    with the bare value ((funcall fn (cdr pair) (car pair))), never boxing
    it in a %php-make-ref or writing a mutated value back afterward. One
    test confirms the callback genuinely is invoked with the right value and
    key (so this isn't a dispatch bug); the other confirms a mutating
    callback's assignment is silently lost. A real fix needs the by-ref
    boxing/writeback array_walk needs to happen dynamically around the
    callback invocation (inspecting the callable's own by-ref-parameter
    declaration at runtime, since the callback isn't known statically the
    way a builtin's own by-ref parameters are), which is a real,
    separately-scoped feature rather than a bounded bug fix — matching the
    date_modify/Fiber resume/ReflectionProperty precedent elsewhere in
    this file, current behavior is documented via a test rather than
    silently left uncovered or half-fixed.
  • t/runtime-builtins-string-analysis-test.lisp: strstr/stristr/
    strchr, str_word_count, levenshtein, similar_text (including
    the bug it caught — see Fixed), soundex, and printf/vsprintf/
    vprintf had no coverage of any kind — no direct unit test and no
    PHP-source e2e test either. runtime-builtins-string-analysis.lisp
    moved from 56.9% to 98.3% sb-cover expression coverage as a
    result; this was the lowest-covered file in the whole src/ tree
    among files with real logic (as opposed to the handful of near-empty
    re-export/dispatch files, or the data tables like
    runtime-constants.lisp, which have an inherent coverage ceiling
    under this metric documented earlier in this file).
  • A test in t/runtime-builtins-io-objects-php85-test.lisp documenting a
    real ReflectionProperty limitation found while investigating
    runtime-builtins-io-reflection-objects.lisp's low coverage:
    %php-reflection-property-new hardcodes __visibility__ to :public
    unconditionally — this runtime has no class-property-visibility
    registry for it to consult instead — so
    %php-reflection-property-get-mangled-name's :private/:protected
    branches are unreachable from any real PHP program, and
    ReflectionProperty::getMangledName() always returns the bare
    property name, even for a genuinely private-declared property (real
    PHP mangles it to "\0ClassName\0propname"). Adding the visibility
    lookup this would need is a real, separately-scoped feature (a
    class-property metadata registry, populated at class-definition time
    and threaded through the constructor), not a bounded bug fix, so —
    matching the precedent set by date_modify and Fiber resume
    earlier in this file — the current behavior is documented via a test
    rather than silently left uncovered or half-fixed.
  • t/runtime-helpers-generators-test.lisp: %php-make-generator (the
    eager-collection, host-only generator path — its own docstring notes
    compiled PHP uses %php-generator-enter/-exit instead, so e2e
    yield tests never reach this path directly), %php-generator-p,
    %php-yield/-from's outside-a-generator inspection-marker fallback,
    and the exception-payload predicates (%php-make-exception,
    %php-exception-object-p/-class/-value/-matches-p) had no direct
    tests. runtime-helpers-generators.lisp moved from 75.4% to 83.8%
    sb-cover coverage.
  • t/runtime-builtins-io-cookie-session-test.lisp: the option-coercion
    helpers (cookie-string/-integer/-bool/-samesite) and the
    Expires HTTP-date formatter (cookie-weekday-name/-month-name/
    -expires-gmt) underneath setcookie/session_* had no direct tests —
    only the high-level builtins they support were e2e-tested.
    runtime-builtins-io-cookie-session.lisp moved from 71.4% to 74.8%
    sb-cover coverage.
  • t/runtime-builtins-regex-date-test.lisp: checkdate (including the
    bug it caught), the leap-year/days-in-month helpers, and the entire
    DateTime-object-style family — date_create/date_format/
    date_modify/date_diff/DateTime::getTimestamp — had no tests at
    all (unlike date()/gmdate()/strtotime(), which are already
    exercised extensively by PHP-source e2e tests). date_modify's test
    documents its current real behavior — a no-op, since this runtime does
    not implement PHP's relative-time-string parser — rather than leaving
    it silently unasserted. runtime-builtins-regex-date.lisp moved from
    59.4% to 82.9% sb-cover coverage.
  • t/parser-php84-features-test.lisp: %php-fiber-suspend's outside-a-fiber
    error path, isRunning/isSuspended (previously only ever seen nil),
    and %php-fiber-resume's two branches — signalling on a fiber that was
    never suspended, and (the more important case) locking in, via a test
    rather than only a comment, that resuming a fiber that did suspend
    terminates it and returns null instead of truly continuing: RESUME-FN
    is never set anywhere in this file (see its own header comment on why),
    so this is this runtime's actual, current behavior, not a hypothetical.
    runtime-fibers.lisp moved from 54.8% to 71.0% sb-cover coverage.
  • t/runtime-builtins-io-files-test.lisp: STDIN/STDOUT/STDERR,
    flock (shared vs. exclusive lock compatibility, and release via
    LOCK_UN), fgetc, fseek/ftell/rewind, and fgetcsv/fputcsv
    had no tests — runtime-builtins-io-streams.lisp moved from 69.2% to
    88.0% sb-cover expression coverage.
  • t/runtime-builtins-math-test.lisp: the entire bcmath (bcadd/bcsub/
    bcmul/bcdiv/bcmod/bccomp/bcscale/bcsqrt/bcpow) and gmp_*
    (gmp_init/gmp_add/gmp_sub/gmp_mul/gmp_div_q/gmp_mod/gmp_pow/
    gmp_abs/gmp_neg/gmp_gcd/gmp_cmp/gmp_intval/gmp_strval/
    gmp_sqrt/gmp_fact) extension families, plus rand/mt_rand/srand/
    mt_srand/random_int/random_bytes, pi, atan2, and is_infinite,
    had no tests at all — unlike runtime-constants.lisp, this is ordinary
    logic with real, closeable branches, not a data table with an inherent
    coverage ceiling. runtime-builtins-math.lisp moved from 54.8% to 95.1%
    sb-cover expression coverage as a result.
  • t/runtime-bridge-provider-test.lisp: %php-host-bridge-entries (the scan
    that finds every %PHP-* function for the VM host bridge) had no direct
    test — only e2e tests observing that its result was usable — and measured
    at 8.0% sb-cover expression coverage as a result. Directly calling it and
    asserting every returned entry is a real, callable, non-macro %PHP-*
    function brought that to 88.0%.
  • t/runtime-constants-test.lisp: every entry in *php-predefined-constants*
    and *php-predefined-class-constants* now round-trips through
    %php-lookup-constant/%php-predefined-class-constant, plus the
    dynamic-constant (STDIN/STDOUT/STDERR) and namespace-qualified lookup
    paths. This moved runtime-constants.lisp only from 65/838 to 69/838
    sb-cover expressions — most of that file's "expressions" are literal data
    inside two large quoted tables (+php-nl-langinfo-items+'s 57 tuples, the
    Intl/Pdo/Uri class-constant registrations' ~30), which read-time literal
    data can't register as "executed" the way a table-driven lookup can, so
    the file's ceiling under this metric is well under 100% regardless of test
    quality.
  • coverage.lisp and packages.coverage in flake.nix (nix build .#coverage):
    an sb-cover HTML report scoped to src/'s own code, following the same
    pattern other nerima-lisp repos (e.g. cl-prolog) use — a package, not a
    check, so it makes the number visible without gating nix flake check on a
    threshold nobody has agreed to yet. cl-nix-forge's mkCoverageReport
    (what those repos actually use) was evaluated first and set aside: it
    requires migrating this repo's whole hand-written flake.nix onto
    cl.lispDerivation, which is a much larger, separately-scoped change for a
    repo whose dependency shape (pulling cl-cc from a monorepo checkout by
    subdirectory) doesn't match cl-nix-forge's single-tagged-dependency model.
  • Two cl-weave it-fuzz tests in t/lexer-test.lisp: 200 trials each of
    tokenize-php-source/parse-php-source against strings drawn from a
    PHP-syntax-heavy alphabet, asserting malformed input always ends in a
    reported simple-error diagnostic (the only outcome the lexer/parser ever
    intentionally signal for bad input) rather than an unhandled crash.
  • t/runtime-builtins-string-json-test.lisp: json_encode (scalars,
    JSON_PRETTY_PRINT), %php-json-quote-string's escape table,
    json_decode's malformed-input-returns-null tolerance, and — the main
    gap — json_validate (see the json_validate entry under Fixed), which
    had zero direct tests despite being a real 8.3 builtin with observable
    pass/fail behaviour.
  • Two tests in t/parser-class-e2e-test.lisp checking member-level
    final function ... — the same "does the sibling of a just-fixed
    feature have the same gap" check applied to final class above.
    Unlike final class, no fix was needed here: final is already in
    %php-parse-visibility-modifiers's accepted modifier list alongside
    :abstract/:readonly (src/parser-class.lisp), so a member-level
    final function seal() { ... } is consumed as an ordinary modifier
    before the :function keyword dispatch and always parsed correctly —
    confirmed by the first test. The second test confirms the same class
    of enforcement gap as final class/readonly elsewhere in this file:
    overriding a final method in a subclass is currently not
    rejected, unlike real PHP's fatal "Cannot override final method"
    error — nothing tracks "is this method final" past parsing to check
    against a subclass's own method table.

Changed

  • The four t/ files over the 500-line limit were split by concern, using
    paredit's structural kill/yank so every move was a verified-balanced
    span, not a hand-edited one: runtime-builtins-io-objects-php85-test's
    eight Uri* tests moved to runtime-builtins-io-uri-php85-test;
    runtime-builtins-io-tokenizer-php85-test's ten
    setcookie/session-cookie-param tests moved to
    runtime-builtins-io-cookie-session-php85-test;
    runtime-builtins-array-test's eleven reshape/compare tests (chunk
    through replace) moved to runtime-builtins-array-transform-test; and
    runtime-builtins-register-e2e-test's seventeen largest tests moved to
    runtime-builtins-register-e2e-formatting-test (a size-driven split,
    documented as such in its header, since the parent suite's own point is
    to stay one flat builtin-reachability list). :components order is
    otherwise unchanged; every new file is registered immediately after the
    file it was split from.
  • flake.nix/flake.lock bumped the three tagged, test-only inputs to their
    latest releases: cl-weave v1.0.0 → v1.1.0, cl-prolog v1.0.1 → v1.1.0,
    cl-parser-kit v1.0.0 → v1.0.1. cl-cc stays pinned at its current commit:
    it is 166 commits behind an unreleased, currently-failing-nix flake check
    branch, so bumping it is its own change with its own test run, not a routine
    dependency update (see docs/src/compatibility.md).
  • src/package.lisp no longer :uses the sibling compiler packages. The 155
    names src/ actually reads from cl-cc/ast, cl-cc/bootstrap, and
    cl-cc/parse are taken in with :import-from, and the :use list names
    only #:cl, both in #: designator form. No symbol identity changed.
  • Five source files over the 500-line limit were split by concern:
    runtime-builtins-array into callable/compare/reshape/cursor pieces,
    php84-features into parser-call-args, parser-property-hooks, and
    runtime-fibers, runtime-builtins-io into -io-ini, -io-locale, and
    -io-autoload, runtime-builtins-io-files into -io-streams, and
    runtime-builtins-register into its machinery plus two name-table files.
    :components order is unchanged for every pre-existing entry, so the
    :serial t load order is the same.
  • class_implements/class_parents/class_uses and the SPL autoload registry
    now load after runtime-builtins-io-objects, removing a forward reference to
    the %php-reflection-* helpers they wrap.
  • No source line exceeds 100 columns. Twelve unbreakable message strings became
    (concatenate 'string ...) of two literals; the messages themselves are
    unchanged.
  • Every file in t/ is named after the src/ file it covers
    (<source>-test.lisp, or <source>-<aspect>-test.lisp), and the two shared
    fixtures are helpers-parser.lisp and helpers-e2e.lisp.

Removed

  • 21 unreferenced function definitions across 14 files (found with
    paredit inspect unused-definitions, cross-checked against every
    t/ test file, src/package.lisp's export list, and the sibling cl-cc
    checkout so exported public API was left untouched): php-lex-peek and
    php-lower-foreach/php-lower-while (both superseded by their
    -with-label variants), %php-apply-named-args,
    %php-parse-arglist-named, %php-parse-first-class-callable,
    %php-named-arg-pair-ast, %php-parse-named-args,
    %php-parse-trait-use-member, %php-parse-property-slot,
    %php-skip-expression-like, %php-slot-readonly-p,
    %php-parse-label-stmt, %php-skip-type-annotation,
    %php-array-map-multi, %php-array-splice-in-place,
    %php-array-map-null, %php-object-visible-pair-by-name, %php-enum-name
    (whose hash-table lookup used the wrong key type, 'name instead of the
    "name" string every enum case is actually stored under, so it could never
    have returned a real value even if it had been called), %php-enum-p, and
    the test helper %php-assert-full-source-unsupported. At the time,
    +php-filter-validate-int+/-float+/-url+/-email+ were left in place
    as incomplete-but-intentional PHP surface rather than dead code, on the
    belief that %php-filter-var's dispatch did not yet handle them — that
    belief was stale even then: the dispatch already routed all four filter
    IDs correctly, just via bare magic numbers (257/259/273/274)
    instead of these named constants, which is why a later, independent
    unused-definitions pass flagged them again as unreferenced. See Fixed
    below for the actual resolution: the dispatch now uses the named
    constants directly.
  • Four more unreferenced definitions, found by a follow-up
    paredit inspect unused-definitions sweep (this time run against
    src/*.lisp and t/*.lisp together, since scoping to src/ alone
    produced 30 candidates — most of them false positives only called from
    t/, which the tool can't see when test files aren't in its input set):
    %php-skip-attributes (src/parser-attributes.lisp, a thin
    discard-the-parse wrapper around %php-parse-attributes that no call
    site ever actually wanted), %php-parse-trait-decl
    (src/parser-trait.lisp, a complete, correct trait Name { ... }
    parser — including its own *php-trait-registry* population — silently
    superseded when trait statement parsing was generalized onto the
    shared %php-parse-classlike used for interfaces/enums too; the live
    registry-population site is src/parser-stmt-decls.lisp's
    %php-parse-classlike, not this file), and %php-generator-send/
    %php-generator-current (src/runtime-helpers-generators.lisp) — real,
    correct implementations of PHP Generator's send()/current() methods
    for this runtime's eager, pre-collected-queue generator model, but
    PHP-level $gen->send(...)/$gen->current() method-call syntax was
    never wired to dispatch to them anywhere in the parser or lowering
    pipeline (generators here only work through foreach/yield from
    iteration, via %php-generator-next/-valid), so neither was ever
    reachable at runtime.
  • The "advisory" method-copying block in %php-apply-traits
    (src/parser-trait.lisp) — found while investigating (and, in the
    same pass, fixing — see Fixed) the trait-methods gap. It was also
    independently buggy: (setf (getf record :methods) ...) mutated a
    local variable bound to (car existing), not the actual
    *php-trait-applications* hash-table entry, so on the very first
    trait in any use list (where :methods doesn't exist in the plist
    yet and setf-of-getf needs to prepend a new key) the update was
    silently discarded every time. Confirmed via grep that nothing
    anywhere read :methods from *php-trait-applications* entries
    (only :trait-names, for reflection, actually was), so this was dead
    code independent of the bug in it. *php-trait-registry* itself —
    correctly populated, just as unread at the time this was found — is
    now genuinely read: it is exactly the data (trait name → member
    slot-defs) %php-merge-trait-members needed for the real fix.
  • A dead (when (fboundp '%php-register-builtin) ...) guard around
    runtime-fibers.lisp's Fiber builtin registrations, flagged by a
    peer session's independent backward-compat audit. %php-register- builtin is defined in runtime-builtins-register.lisp, loaded at
    position 79 of cl-cc-php.asd's component list; runtime-fibers.lisp
    loads at position 113 — strictly after — so the guard could never
    actually be false. Cross-checked the same audit's other two findings
    (%php-ref-p/%php-generator-p, both flagged as possibly-unreachable
    thin wrappers around their defstruct's own predicate) against this
    session's current code and the sibling cl-cc* checkouts before acting
    on them: %php-ref-p turned out to have a live call site this same
    session's similar_text fix added
    (runtime-builtins-string-analysis.lisp), and %php-generator-p has
    direct, deliberate unit-test coverage in
    t/runtime-helpers-generators-test.lisp — unlike %php-generator-send/
    -current above, which had zero references anywhere before their
    removal. Both left in place; only the confirmed always-true guard was
    removed.

Fixed

  • t/runtime-builtins-math-test.lisp (formerly t/php-math-tests.lisp) is
    now listed in cl-cc-php.asd, so its assertions are compiled and run. It
    previously was not, and running it for the first time surfaced six test
    bugs (not implementation bugs), now fixed in the same file: exact-rational
    vs. double-float literal comparisons (%php-round's 3.14 case,
    %php-log's (exp 1) case using single- instead of double-float
    precision), CL's (sin 0)/(cos 0)/(tan 0) returning single-float where
    %php-sin/-cos/-tan return double-float, %php-log10/%php-log2's
    documented exact-integer-power short-circuit compared against a float
    literal instead of the integer it actually returns, intdiv's
    division-by-zero-error test targeting a condition class that has never
    existed (%php-throw always signals php-exception, with the PHP class
    name as a slot, not a distinct condition type), and is_finite's
    expectation for MOST-POSITIVE-DOUBLE-FLOAT contradicting this runtime's
    own consistent, pre-existing choice to represent PHP's INF as that same
    sentinel value (see runtime-constants.lisp and %php-fdiv).
  • Stale packages/php/src/... paths in seventeen source-file header comments,
    left over from the extraction out of the cl-cc monorepo.