Releases: nerima-lisp/cl-cc-php
Releases · nerima-lisp/cl-cc-php
Release list
v0.1.1
Changed
t/runtime-builtins-io-objects-php85-test.lispgrew back over the
500-line limit (511 lines) as later work in this session's own
history added more tests to it. Its sixDom\Element/
Dom\ParentNode/Dom\HTMLDocumenttests 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 outruntime-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; nosrc/file is over
500 lines (runtime-builtins-core.lispsits exactly at it).- Every
sbcl --scriptinvocation inflake.nix(checks.default,
packages.coverage, andapps.test/nix run .#test) now runs under
timeout(600s for the plain test suite and the interactivenix 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 carrytimeout-minuteson 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 iscl-cc-php's first adoption of a
nerima-lisp sibling package as a genuine runtime dependency (cl-weave,
cl-prolog, andcl-parser-kitare test-only): it is now a:depends-on
of the shippedcl-cc-phpsystem, not justcl-cc-php/test, wired into
flake.nix/run-tests.lisp/coverage.lispthe same way the cl-cc
subsystems are, and documented indocs/src/installation.md's dependency
table.%php-json-decode/%php-json-encodedeliberately still use the
original hand-rolled reader/writer: PHP'sjson_decodeneeds 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 thanjson_validate's zero-value-model swap.
Fixed
final class Foo { ... }had the exact same gapabstract 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 barefinalkeyword. Found by re-checking the
abstract-class fix's own investigation notes, which had already
identifiedfinalas sharing the same gap without fixing it at the
time. Fixed the same way, registering a:finalstatement parser
mirroring:abstract's. As withabstract, 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
enumusing 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-constantstest
combinesenum ... { 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-defclassat all —%php-parse-classlikewraps an enum's class
definition in anast-prognalongside a%php-enum-finalizecall,
unlike a plain class, whoseast-defclassis a bare top-level form —
so the merge silently never ran for any enum. Fixed by also checking
one level into anast-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 :instancea trait method carries
from where it was declared —%php-parse-classlikealready 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.:readonlyhas its own top-level statement parser
(readonly class Foo {...},src/parser-class.lisp) that requires
classto immediately follow and delegates to
%php-parse-class-decl;:abstract(and:final) had no such
registration, so a leadingabstractbeforeclassfell through to
generic expression-statement parsing and errored on the bare keyword.
Member-levelabstract function ...;inside a class/trait/interface
body already worked (%php-parse-visibility-modifiersalready
recognized:abstractthere) — only the class-level modifier itself
was missing. Found while investigating whether abstract classes work
at all (no test anywhere exercised the class-levelabstract
modifier before this). Fixed by registering a:abstractstatement
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()whereShape
isabstract) 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
withThe 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
onlyclass_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-USEmarker
slot-def (%php-parse-use-trait-stmt,src/parser-trait.lisp) that
nothing in class lowering ever consulted — confirmed withgrep,
there was no reference to:php-trait-use/:php-trait-names
anywhere outsideparser-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 fromparse-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
insteadofclause (TraitA::member insteadof TraitB;keeps TraitA's
version); a collisioninsteadofdoes 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 thatuse ... { 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 rightinsteadof/aliasdata) is unchanged, only the
channel they read it through.asaliasing 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-modifiersvisibility 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
`$v...