Skip to content

MVP: write generics, compile, and deploy#1

Merged
math3usmartins merged 5 commits into
mainfrom
feat/symfony-bundle-and-demo
Jun 3, 2026
Merged

MVP: write generics, compile, and deploy#1
math3usmartins merged 5 commits into
mainfrom
feat/symfony-bundle-and-demo

Conversation

@math3usmartins

Copy link
Copy Markdown
Member

What's in it

Bundle (src/)

  • XphpBundle (AbstractBundle) -- config tree (source / target / cache /
    hash_length / compile_on_warmup / register_runtime_autoloader / services)
    and the wiring that turns declared generics into DI services.
  • XphpCompiler -- drives the transpiler's public console facade in-process;
    uniform CompilationFailedException on any failure.
  • xphp:compile command and a mandatory, fail-loud cache warmer, so
    cache:warmup (run by every prod build) produces the generated PHP -- and a
    compile error aborts the build rather than shipping stale output.
  • GeneratedClassLoader -- runtime SPL autoloader for the XPHP\Generated\*
    specializations (no composer dump-autoload needed in dev).
  • GenericServiceRegistrar -- resolves each declared binding to the
    monomorphized class via the transpiler's own Registry::generatedFqn and
    registers it, honoring per-binding autowire / autoconfigure / public /
    shared / tags over inheritable _defaults.

Opt-in DI for generics

Declare instantiations under xphp.services.bindings; the bundle compiles + registers
them at container-build time (so they're reflectable for autowiring/autoconfigure):

xphp:
  services:
    bindings:
      - template: 'App\Generic\CachedFinder'
        args: ['App\Entity\User']

Because only .xphp can name a generic, the consumer of a Foo<Bar> is itself
.xphp (a service, command, or controller) and injects the specialization directly.

Known stopgap: monomorphization is usage-driven, so a config-only generic
(never new-ed in source) has no specialization to register. The bundle bridges
this by synthesizing a throwaway instantiation per binding at compile time
(transparent, cleaned up afterwards). The clean fix is an upstream "specialization
roots" API in xphp -- a follow-up.

Demo (demo/)

A console-only Symfony app consuming the bundle via a Composer path repository, showing
both patterns side by side:

  • app:demo -- value-type generic: Box<Plastic> monomorphized and called from
    compiled code.
  • app:find -- generic service via DI: an .xphp command autowires a
    CachedFinder<User> (whose own CacheInterface + source-service deps are
    autowired), demonstrating cache-aside.

Notes / follow-ups

  • Library, so no composer.lock is committed (CI resolves latest compatible deps).
  • The synthetic-roots stopgap -> upstream xphp "specialization roots" API.
  • lazy / bind per-binding knobs deferred; the schema extends to them trivially.

How to try it locally

make test            # unit
make test/mutation   # mutation (100% MSI)
cd demo && composer install
bin/console app:find
bin/console app:demo

math3usmartins and others added 5 commits June 3, 2026 19:07
Introduce the xphp-lang/xphp-symfony-bundle: write .xphp generics in a
Symfony app, compile them to vanilla PHP during the normal build, and
autoload the generated classes.

Bundle:
- XphpBundle (AbstractBundle) with source/target/cache, hash_length,
  compile_on_warmup and register_runtime_autoloader config.
- XphpCompiler drives xphp's public ApplicationConsole `compile` facade
  in-process.
- xphp:compile console command for on-demand compilation.
- XphpCacheWarmer hooks compilation into cache:warmup; mandatory and
  fail-loud so a compile error aborts the build instead of shipping
  missing/stale generated PHP.
- GeneratedClassLoader: runtime SPL autoloader for XPHP\Generated\*.

Demo (demo/): console-only Symfony app consuming the bundle via a path
repository. .xphp and plain .php coexist in src/; an app:demo command
calls the compiled Catalog and prints the monomorphized Box<Plastic>
class. Verified end to end: composer install, xphp:compile, app:demo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Let service-like generics participate in Symfony's container. A new
`xphp.services.bindings` config declares which instantiations become
services; the bundle resolves each to the transpiler's monomorphized
class (via the public Registry::generatedFqn) and registers it, with
per-binding autowire/autoconfigure/public/shared/tags over inheritable
_defaults.

- GenericServiceRegistrar builds TypeRefs from explicit template/args
  (nested supported) and creates the Definition + optional alias.
- Registration happens in XphpBundle::loadExtension so autoconfigure
  flows through the normal passes; gated on non-empty bindings.
- XphpCompiler gains specialization "roots": monomorphization is
  usage-driven, so a config-only generic (never `new`-ed in source) is
  bridged with a synthetic, cleaned-up instantiation. Stopgap until xphp
  can accept roots directly.
- Resolve %kernel.project_dir% etc. against the parameter bag before
  touching the filesystem at build time.

Demo: a cache-aside CachedFinder<T> (injecting CacheInterface + a source
service) consumed by FindCommand.xphp — an .xphp command that takes
CachedFinder<User> as a constructor argument and injects the
specialization directly. The generic implements no interface (a
T-generic can't honestly implement a User-specific one); only .xphp can
name a generic, so the consumer is .xphp. Verified: #[AsCommand] and use
statements survive compilation, the ctor type rewrites to the hash, and
autowiring resolves it with its real deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously only the two unit-tested files were in Infection's scope (84%
covered MSI over a small slice). Bring the whole bundle under test and
mutation coverage.

Tests added:
- XphpBundleTest: configure() defaults + bounds, loadExtension param/
  service wiring, the DI binding registration (service id, di_roots,
  resolved-path compile, source resource), build-time autoloader
  registration, and boot().
- XphpCompilerTest: compile output, synthetic-roots generation +
  cleanup, missing-source and transpiler-error paths.
- XphpCacheWarmerTest, CompileCommandTest: enabled/disabled/missing-source
  and success/failure paths.
- TempProjectTrait: throwaway .xphp project fixtures.

Source refinements (remove behavior-invisible mutants instead of masking):
- GeneratedClassLoader: track registered roots as a list (the boolean
  marker value was never read).
- GenericServiceRegistrar: drop redundant ltrim() — Registry::generatedFqn
  and TypeRef::canonical already strip leading backslashes.
- XphpCompiler: always buffer; setCatchExceptions(true) so any transpiler
  error becomes a uniform CompilationFailedException (also testable); drop
  the unused output-streaming param.
- CompileCommand: drop the unreachable empty-summary branch.

infection.json5 documents the remaining ignores — all provably
invisible: mkdir() permission bits, the redundant explicit
setCatchExceptions(true), the synthetic file's exact byte layout, the
(string) casts on resolveValue(), and the empty-roots cleanup guard.

Covered MSI 100% (was 84%); Makefile gate raised to --min-msi=100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitHub Actions workflow runs `make test` (PHPUnit) and
`make test/mutation` (Infection, --min-msi=100) on PHP 8.4 with PCOV,
triggered on pull requests targeting main and on pushes to main (PR
merges). Calling the Makefile targets keeps a single source of truth for
how the suite runs locally and in CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a parallel `demo` job that installs the demo (which consumes the
bundle via a path repository) and runs both console commands, asserting
their output: app:find proves the generic-service DI path (autowired
CachedFinder<User>, cache-aside), app:demo proves the value-type path
(monomorphized Box<Plastic>). A non-zero exit or missing output fails the
build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@math3usmartins math3usmartins merged commit 0558bd4 into main Jun 3, 2026
2 checks passed
@math3usmartins math3usmartins deleted the feat/symfony-bundle-and-demo branch June 3, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant