Skip to content

Commit c30d837

Browse files
authored
feat(emit-java): add a dependency-free Java 17 emitter target (#858) (#1069)
* chore(#858): scaffold draft PR for the Java emitter target * feat(emit-java): backend skeleton, runtime, provider registration * feat(emit-java): Java naming + type mapping * feat(emit-java): expression translator * feat(emit-java): value objects + generated IDs as validating records * feat(emit-java): smart enums as Java enums * feat(emit-java): entities + aggregates as invariant-guarded classes * feat(emit-java): events, commands, repositories * test(emit-java): javac conformance harness + snapshots * docs(emit-java): document the Java target across README, USER-STORIES, website * fix(emit-java): compile clean under javac 17 (cross-context types, Range, value-object arithmetic) Compiling the emitted Java with a real JDK 17 surfaced three gaps the string/snapshot tests missed (the billing + pizzeria starter templates did not compile): - Cross-context type references now package-qualify (`<base>.<ownerContext>.Type`), mirroring how the Rust emitter qualifies `crate::<module>::Type`; local types stay bare. - A `koine.runtime.Range<T>` record is emitted (Koine `Range<T>` mapped to it but it never existed), alongside `DomainException`. - Value-object arithmetic lowers to methods (Java has no operator overloading): demand-driven `plus`/`minus`/`times`/`dividedBy` on value objects via OperatorNeedsAnalyzer, and the translator lowers VO `+`/`-`/`*`/`/` and `sum`-of-VO to them instead of raw operators. Both starter templates now compile with `javac --release 17` (0 errors); a new conformance fixture exercises all three categories. * fix(emit-java): address code-review findings (invalid Java from edge-case models) Six bugs a real javac 17 rejects, found in review (verified fixed under JDK 17): - Decimal `/` now passes `MathContext.DECIMAL128` (bare BigDecimal.divide throws ArithmeticException on non-terminating quotients). - A negated Decimal literal folds the sign into the string (`new BigDecimal("-273.15")`) instead of `-new BigDecimal(...)` (no unary minus on BigDecimal). - Built-in member-ops (count/length/trim/…) only fire when the receiver actually supports them, so a domain member named `count`/`length` no longer emits a phantom `.size()`. - A large positive int literal in a BigDecimal operand keeps its `L` suffix. - JavaNaming escapes the record-component-illegal Object method names (hashCode, toString, notify, wait, clone, finalize, getClass, notifyAll). - Equality on an optional (`Int?`/`Bool?`) routes through Objects.equals, not raw `==`. Adds a javac-17 regression conformance fixture covering all six. * feat(studio): expose the Java emit target in the IDE Add Java to the offline `BUILTIN_EMIT_TARGETS` fallback (the live list is already registry-seeded) so the output-language picker, generate-project wizard, and assistant compile-tool all offer it, and highlight emitted `.java` via the bundled clike CodeMirror mode. * docs(emit-java): surface Java on the website roadmap, reference overview, and landing page * fix(emit-java): root Koine.Emit.Java as a wasm trimmer assembly
1 parent 8da8b61 commit c30d837

47 files changed

Lines changed: 5047 additions & 23 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Koine.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Project Path="src/Koine.Emit.CSharp/Koine.Emit.CSharp.csproj" />
1919
<Project Path="src/Koine.Emit.Docs/Koine.Emit.Docs.csproj" />
2020
<Project Path="src/Koine.Emit.Glossary/Koine.Emit.Glossary.csproj" />
21+
<Project Path="src/Koine.Emit.Java/Koine.Emit.Java.csproj" />
2122
<Project Path="src/Koine.Emit.OpenApi/Koine.Emit.OpenApi.csproj" />
2223
<Project Path="src/Koine.Emit.Php/Koine.Emit.Php.csproj" />
2324
<Project Path="src/Koine.Emit.Python/Koine.Emit.Python.csproj" />

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[![Documentation](https://img.shields.io/badge/docs-koine-3245b8)](https://atypical-consulting.github.io/Koine/)
99
[![.NET](https://img.shields.io/badge/.NET-10-512BD4)](https://dotnet.microsoft.com/)
1010
[![Tests](https://img.shields.io/badge/tests-1900%2B%20passing-2ea44f)](tests/)
11-
![Target](https://img.shields.io/badge/emits-C%23%20%C2%B7%20TypeScript%20%C2%B7%20Python%20%C2%B7%20PHP%20%C2%B7%20Rust%20%C2%B7%20docs%20%C2%B7%20AsyncAPI%20%C2%B7%20OpenAPI-178600)
11+
![Target](https://img.shields.io/badge/emits-C%23%20%C2%B7%20TypeScript%20%C2%B7%20Python%20%C2%B7%20PHP%20%C2%B7%20Rust%20%C2%B7%20Java%20%C2%B7%20docs%20%C2%B7%20AsyncAPI%20%C2%B7%20OpenAPI-178600)
1212
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
1313

1414
## The problem
@@ -42,6 +42,12 @@ aggregates with invariant-checked behaviors, factories that mint identities, dom
4242
a `Vec`-friendly `DomainEvent` collection, query DTOs and read-model projections, and repositories as
4343
`trait`s; **multi-context** models compile end-to-end via `crate::<module>` qualification; depends only
4444
on `rust_decimal` for money and `regex` for `matches`, plus `uuid` when a model uses a factory), a
45+
**Java 17** emitter ships (`--target java` → dependency-free, stdlib-only Java 17: value objects and
46+
domain events as validating `record`s with compact constructors, smart enums as Java `enum`s (with
47+
per-constant associated data), entities and aggregates as classes with identity `equals`/`hashCode` and
48+
invariant-guarded behaviors, generated identities as branded `record`s minting `java.util.UUID`, domain
49+
events grouped under a per-context `sealed interface DomainEvent`, and repositories as `interface`s —
50+
one public type per `.java` file, `koine.runtime.DomainException` for invariant failures), a
4551
**docs** target emits living
4652
documentation (`--target docs` → Markdown + Mermaid diagrams) straight from the model, an
4753
**AsyncAPI 3.0** target emits a single event-API document (`--target asyncapi` → channels, messages,
@@ -76,7 +82,7 @@ surface below is a click away in the [live Studio](https://atypical-consulting.g
7682
| IDE view | What it shows |
7783
|----------|---------------|
7884
| **Editor & live diagnostics** | Your `.koi` model on the left, the emitted code on the right, with error squiggles and quick info as you type — the same parser and validator the CLI runs. |
79-
| **Emit-target switcher** | Flip the *same* model between **C#**, **TypeScript**, **Python**, **PHP**, and **Rust** output without leaving the page. |
85+
| **Emit-target switcher** | Flip the *same* model between **C#**, **TypeScript**, **Python**, **PHP**, **Rust**, and **Java** output without leaving the page. |
8086
| **Context-map graph** | An interactive graph of the bounded contexts and the relationships between them. |
8187
| **Diagram views** | Aggregates and state machines rendered as diagrams straight from the model. |
8288
| **Ubiquitous-language glossary** | The generated glossary of every term in the domain, kept in lock-step with the model. |
@@ -206,6 +212,9 @@ dotnet run --project src/Koine.Cli -- build templates/starters/billing/billing.k
206212
# Or to Rust (multi-context + CQRS — an idiomatic crate; `cargo build` proves it compiles)
207213
dotnet run --project src/Koine.Cli -- build templates/starters/billing/billing.koi --target rust --out ./generated_rs
208214

215+
# Or to Java (dependency-free Java 17 — records, sealed events, invariant-guarded classes; `javac --release 17` proves it compiles)
216+
dotnet run --project src/Koine.Cli -- build templates/starters/billing/billing.koi --target java --out ./generated_java
217+
209218
# Emit the opt-in C# Application layer alongside the domain (handlers, validators, query handlers, DI)
210219
dotnet run --project src/Koine.Cli -- build templates/starters/billing/billing.koi --target csharp --layers domain,application --out ./generated
211220

USER-STORIES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ context Sales version 3 {
819819

820820
_BRIEF §9 and the README position TypeScript and Rust as the proof that the Ast/ + IEmitter seam is genuinely target-agnostic; today only CSharpEmitter exists and the CLI hard-rejects any non-csharp target (Program.cs). Rust in particular forces error handling to map to `Result<T,E>` instead of the exception-based DomainInvariantViolationException — the strongest test of the seam. Real projects also need to configure the existing C# emitter (namespace mapping, NodaTime mode — still a literal 'TODO: NodaTime' in CSharpTypeMapper, output layout). This is the capstone of the long-term vision, sequenced last because it benefits from a mature, stable AST._
821821

822-
> **✅ Delivered.** R16 shipped: C# emitter configuration (R16.1), a full TypeScript emitter (R16.2), a Rust emitter (R16.3 — **Phase 1: tactical core**; remaining breadth tracked in [#173](https://github.com/Atypical-Consulting/Koine/issues/173)), and the cross-emitter conformance harness with an `Ast/`-purity guard (R16.4). The seam held: every target-specific decision lives in its `Emit/<Target>/` emitter and `Ast/` gained no error-handling or target concept — Python and PHP emitters followed the same seam. `koine init` still emits a forward-compatible `koine.config` whose `targets.*` block configures the C# emitter.
822+
> **✅ Delivered.** R16 shipped: C# emitter configuration (R16.1), a full TypeScript emitter (R16.2), a Rust emitter (R16.3 — **Phase 1: tactical core**; remaining breadth tracked in [#173](https://github.com/Atypical-Consulting/Koine/issues/173)), and the cross-emitter conformance harness with an `Ast/`-purity guard (R16.4). The seam held: every target-specific decision lives in its `Emit/<Target>/` emitter and `Ast/` gained no error-handling or target concept — Python, PHP, and a dependency-free **Java 17** emitter (`--target java`, Phase 1: tactical core + events/commands/repositories, [#858](https://github.com/Atypical-Consulting/Koine/issues/858)) followed the same seam. `koine init` still emits a forward-compatible `koine.config` whose `targets.*` block configures the C# emitter.
823823
824824
### R16.1 C# emitter configuration (namespaces, NodaTime, layout) · 🟡 Medium
825825
**Delivered***As an Architect, I want a structured emitter options object to map contexts to concrete namespaces, choose the Instant mapping, and control output layout, so that generated code drops into our existing project conventions.*
@@ -924,5 +924,5 @@ _With the MCP server (`src/Koine.Mcp`) an AI agent can now author a complete `.k
924924
- Koine Studio's shared design-system layer — the `--koi-*` design tokens, framework-free DOM/interaction primitives, and store-free presentational components — was extracted into a standalone workspace package, [`@atypical/koine-ui`](tooling/koine-ui/) (issue #905), so a second surface (the docs-site playground today, a future embed or IDE tomorrow) can reuse Koine Studio's look and interaction patterns without pulling in the whole IDE.
925925
- Example domains live in [`templates/`](templates/) — the single, CI-validated source of truth (issue #101). Each template is a folder of `.koi` files plus a `template.json` manifest (validated against [`templates/template.schema.json`](templates/template.schema.json)); `TemplatesValidationTests` compiles every template green, and the set powers the demo, Koine Studio's gallery, and the website playground.
926926
- The [`demo/`](demo/) project (`Pizzeria.Domain`) compiles the [`templates/pizzeria`](templates/pizzeria) template in place — six bounded contexts plus an external Gateway — exercising the full shipped surface, and is the natural place to validate new stories end-to-end.
927-
- Sequencing rationale: **R1–R4** sharpen the existing surface (expressions, optionality, diagnostics, docs) at low risk; **R5–R10** add the missing tactical behaviour (commands, events, lifecycle, factories, richer value objects, specifications/services/policies); **R11–R12** add the persistence & application abstractions; **R13–R15** unlock multi-file and strategic design; and **R16–R18** are delivered — multi-target emitters (TypeScript, Python, PHP, and Rust Phase 1) proved the `Ast/`+`IEmitter` seam is target-agnostic, the editor tooling rounded out developer experience, and the model-as-spec coverage analyzer (`koine coverage`) closed the declared-equals-emitted loop.
927+
- Sequencing rationale: **R1–R4** sharpen the existing surface (expressions, optionality, diagnostics, docs) at low risk; **R5–R10** add the missing tactical behaviour (commands, events, lifecycle, factories, richer value objects, specifications/services/policies); **R11–R12** add the persistence & application abstractions; **R13–R15** unlock multi-file and strategic design; and **R16–R18** are delivered — multi-target emitters (TypeScript, Python, PHP, Rust Phase 1, and Java Phase 1) proved the `Ast/`+`IEmitter` seam is target-agnostic, the editor tooling rounded out developer experience, and the model-as-spec coverage analyzer (`koine coverage`) closed the declared-equals-emitted loop.
928928

src/Koine.Emit.All/BuiltInEmitterProviders.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Koine.Compiler;
77
/// truth for which targets ship with the compiler; the CLI and MCP registries both seed an
88
/// <see cref="EmitterRegistry"/> from here so they can never drift (issue #69, Task 5). Each provider
99
/// now lives in its own <c>Koine.Emit.&lt;Target&gt;</c> assembly (issue #861); this aggregator is the
10-
/// one place that references all nine, so <see cref="EmitterRegistry"/> (in the core compiler) can take
10+
/// one place that references them all, so <see cref="EmitterRegistry"/> (in the core compiler) can take
1111
/// the list as input rather than reaching down into the emitter assemblies (which would be a cycle).
1212
/// </summary>
1313
public static class BuiltInEmitterProviders
@@ -20,6 +20,7 @@ public static class BuiltInEmitterProviders
2020
new PythonEmitterProvider(),
2121
new PhpEmitterProvider(),
2222
new RustEmitterProvider(),
23+
new JavaEmitterProvider(),
2324
new GlossaryEmitterProvider(),
2425
new DocsEmitterProvider(),
2526
new AsyncApiEmitterProvider(),

src/Koine.Emit.All/Koine.Emit.All.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99

1010
<!-- The built-in emitter aggregator (issue #861): owns BuiltInEmitterProviders, the single source of
1111
truth for which targets ship with Koine, and references every Koine.Emit.<Target> so a host gets
12-
"one reference, all nine targets" (the meta-package preserving the #69 platform experience).
12+
"one reference, all targets" (the meta-package preserving the #69 platform experience).
1313
Assembly name (Koine.Emit.All) != namespace (Koine.Compiler.Emit) so consumer churn is
1414
reference-only. -->
1515
<PropertyGroup>
1616
<IsPackable>true</IsPackable>
1717
<PackageId>Koine.Emit.All</PackageId>
18-
<Description>The Koine emitter meta-package — references every built-in Koine.Emit.&lt;Target&gt; backend (C#, TypeScript, Python, PHP, Rust, glossary, docs, AsyncAPI, OpenAPI) and exposes BuiltInEmitterProviders, the single source of truth for the targets that ship with Koine.</Description>
18+
<Description>The Koine emitter meta-package — references every built-in Koine.Emit.&lt;Target&gt; backend (C#, TypeScript, Python, PHP, Rust, Java, glossary, docs, AsyncAPI, OpenAPI) and exposes BuiltInEmitterProviders, the single source of truth for the targets that ship with Koine.</Description>
1919
<PackageTags>ddd;domain-driven-design;dsl;compiler;code-generation;koine;emitter</PackageTags>
2020
</PropertyGroup>
2121

@@ -32,6 +32,7 @@
3232
<ProjectReference Include="..\Koine.Emit.Python\Koine.Emit.Python.csproj" />
3333
<ProjectReference Include="..\Koine.Emit.Php\Koine.Emit.Php.csproj" />
3434
<ProjectReference Include="..\Koine.Emit.Rust\Koine.Emit.Rust.csproj" />
35+
<ProjectReference Include="..\Koine.Emit.Java\Koine.Emit.Java.csproj" />
3536
<ProjectReference Include="..\Koine.Emit.Glossary\Koine.Emit.Glossary.csproj" />
3637
<ProjectReference Include="..\Koine.Emit.Docs\Koine.Emit.Docs.csproj" />
3738
<ProjectReference Include="..\Koine.Emit.AsyncApi\Koine.Emit.AsyncApi.csproj" />

src/Koine.Emit.Common/Koine.Emit.Common.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<InternalsVisibleTo Include="Koine.Emit.Python" />
4040
<InternalsVisibleTo Include="Koine.Emit.Php" />
4141
<InternalsVisibleTo Include="Koine.Emit.Rust" />
42+
<InternalsVisibleTo Include="Koine.Emit.Java" />
4243
<InternalsVisibleTo Include="Koine.Compiler.Tests" />
4344
</ItemGroup>
4445

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Koine.Compiler.Ast;
2+
using Koine.Compiler.Emit;
3+
4+
namespace Koine.Compiler;
5+
6+
/// <summary>
7+
/// The aggregate slice of <see cref="JavaEmitter"/>. A Koine <c>aggregate</c> is a consistency boundary,
8+
/// not a Java type of its own: its nested types are emitted <b>flat</b> into the context package — each
9+
/// value object, entity (with its generated identity), enum, and event getting its own <c>.java</c> file
10+
/// via the same <see cref="EmitType"/> dispatch as a top-level declaration. The aggregate root is simply
11+
/// the nested entity named by <see cref="AggregateDecl.RootName"/>, so its class (invariant-guarded, with
12+
/// identity equality and any recorded domain events) already falls out of the entity slice. Mirrors the
13+
/// Rust backend's aggregate handling (recurse <c>agg.Types</c>, then the aggregate extras); the root's
14+
/// persistence-ignorant repository <c>interface</c> is the follow-on events/repositories task.
15+
/// </summary>
16+
public sealed partial class JavaEmitter
17+
{
18+
/// <summary>Emits an aggregate by recursing into each nested type (one <c>.java</c> file each); the root entity is one of them.</summary>
19+
private void EmitAggregate(JavaEmitContext emit, List<EmittedFile> files, string context, AggregateDecl agg)
20+
{
21+
foreach (TypeDecl nested in agg.Types)
22+
{
23+
EmitType(emit, files, context, nested);
24+
}
25+
26+
// The aggregate root's persistence-ignorant repository interface (the Rust backend's
27+
// EmitAggregateExtras analogue). The per-context DomainEvent sealed interface is emitted once per
28+
// context by EmitContextExtras, not here, so a same-named event nested in the aggregate still lands
29+
// in the single context-wide sum.
30+
EmitAggregateExtras(emit, files, context, agg);
31+
}
32+
}

0 commit comments

Comments
 (0)