Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

The next release is **breaking**, so it needs a `0.2.0` version bump: under
SemVer a `0.x` crate signals incompatibility by raising the minor. Several
changes since `0.1.0` alter the public model, this one included.

### Added

- Generation-time validation: `KtFile::validate` / `validate_with`,
`Diagnostic`, `Check`, `Severity` and `ValidationPolicy`, run by
`merge_files` and `write_files`. `merge_files_with` / `write_files_with`
expose per-check severities and return surviving warnings.
- Kotlin identifier utilities: `is_valid_kotlin_ident`, `mangle_kotlin_ident`,
`escape_kotlin_ident`, `is_escaped_kotlin_ident`, `is_writable_kotlin_ident`,
`is_kotlin_hard_keyword`, `KOTLIN_HARD_KEYWORDS`, plus the package-path
equivalents.
- Extension functions: `receiver` on `KtFun` and `KtFunSig`, with
`KtType::render_receiver` for the parentheses a function-type receiver needs.
- `open` and `sealed` classes, named companion objects, and `KtFunSig` for
abstract members.
- `KOTLIN_BANNER` and `merged_file_path` are exported; both were previously
`pub` inside private modules and unreachable.
- Two runnable examples, `showcase` and `invalid`, pinned by golden files.

### Changed — breaking

- `KtClassKind` variants carry their own data, so constructor parameters,
enum entries and the class modifier live on the kind rather than on
`KtClass`. `Plain`/`Abstract` became `Class { modifier }`, and `Companion`
and `ValueInline` are gone.
- A companion object is `KtCompanion`, reached only through
`KtClass::companion`; `KtClass::companion_object()` is removed.
- Supertypes are `KtSupertypes { superclass, interfaces }`;
`KtClass::supertype()` is replaced by `extends` and `implements`.
- `KtFunInterface::method` is a `KtFunSig`, which cannot carry a body.
- `external` is a `KtBody` variant set by `KtFun::external()`, not a modifier
string.
- `KtFun` and `KtFunSig` gained a `receiver` field. Callers using the builders
are unaffected; a caller constructing either with a **struct literal** must
add it.

## 0.1.0

Initial release.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ cargo run --example invalid # a broken model, and what the validator says
`showcase` is a small but complete generator — it builds `KtFile` fragments,
merges them so each package collapses to one file, and renders classes,
objects, enums, data and value classes, sealed interfaces, `fun interface`s,
type aliases, properties with accessors and delegates, `external` natives, and
raw blocks. `invalid` shows the two ways a mistake surfaces: as a diagnostic
type aliases, extension functions, properties with accessors and delegates,
`external` natives, and raw blocks. `invalid` shows the two ways a mistake surfaces: as a diagnostic
from the validator, or as a builder that refuses to construct the value at all.

To accept an intended change to either output:
Expand Down
18 changes: 18 additions & 0 deletions examples/invalid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@ fn broken_declarations() -> KtFile {
.returns(KtType::long())
.body(KtCode::new()),
)
// Extensions are keyed on their receiver, so these two collide while
// the same pair on different receivers would not.
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.example.Codec"))
.body(KtCode::new()),
)
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.example.Codec"))
.body(KtCode::new()),
)
// ...as here: same name, different receiver, no diagnostic.
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.example.Other"))
.body(KtCode::new()),
)
// Names that are not legal Kotlin identifiers.
.decl(
KtClass::class_("My-Class")
Expand Down
78 changes: 77 additions & 1 deletion examples/showcase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,23 @@ fn session_fragment() -> KtFile {
.line("return acc"),
),
)
// The interface's member extension, implemented.
.member(
KtFun::new("label")
.vis(KtVis::Public)
.modifier("override")
.receiver(KtType::cls("Sample"))
.returns(KtType::string())
.expr_body(KtCode::new().line("\"${keyExpr}@${describe()}\"")),
)
// A member extension of the class itself.
.member(
KtFun::new("toSample")
.vis(KtVis::Public)
.receiver(KtType::byte_array())
.returns(KtType::cls("Sample"))
.expr_body(KtCode::new().line("Sample(describe(), this)")),
)
// A nested class — its own scope, so its members may reuse names.
.member(
KtClass::class_("Config")
Expand All @@ -270,7 +287,15 @@ fn session_fragment() -> KtFile {
// `KtFunSig` is exactly that.
let describable = KtClass::interface_("Describable")
.vis(KtVis::Public)
.member(KtFunSig::new("describe").returns(KtType::string()));
.member(KtFunSig::new("describe").returns(KtType::string()))
// A member extension: abstract here, supplied by the implementor.
// `KtFunSig` carries a receiver too, so a signature does not quietly
// become a plain member.
.member(
KtFunSig::new("label")
.receiver(KtType::cls("Sample"))
.returns(KtType::string()),
);

// A `fun interface` (SAM) — its single method cannot carry a body.
let handler = KtFunInterface::new(
Expand All @@ -283,13 +308,57 @@ fn session_fragment() -> KtFile {
.type_param("out R")
.kdoc("Invoked from the native thread for each reply.");

// Top-level extension functions. The receiver is a type, not part of the
// name, so it resolves through the import set and the name stays a plain
// identifier the validator can check as one.
let summary = KtFun::new("summary")
.vis(KtVis::Public)
.receiver(KtType::cls("Sample"))
.returns(KtType::string())
.expr_body(KtCode::new().line("\"$keyExpr (${payload.size} bytes)\""));

// Generics render before the receiver, which renders before the name.
let map_replies = KtFun::new("mapValues")
.vis(KtVis::Public)
.generic("R")
.receiver(KtType::generic("List", [KtType::cls("Reply")]))
.param(KtParam::new(
"transform",
KtType::lambda(
[("sample".to_string(), KtType::cls("Sample"))],
KtType::var_r(),
),
))
.returns(KtType::generic("List", [KtType::var_r()]))
.expr_body(
KtCode::new().line("filterIsInstance<Reply.Value>().map { transform(it.sample) }"),
);

// An extension on a *function type*. The receiver needs parentheses here
// or the `.` would bind to the return type instead.
let as_raw = KtFun::new("asRaw")
.vis(KtVis::Internal)
.receiver(KtType::lambda(
[("sample".to_string(), KtType::cls("Sample"))],
KtType::unit(),
))
.returns(KtType::cls("io.example.api.internal.RawSink"))
// The proxy adapts a typed callback to the raw one the natives call,
// so it has to narrow `Reply` to the `Sample` the receiver takes.
.expr_body(
KtCode::new().line("RawSink { raw -> if (raw is Reply.Value) this(raw.sample) }"),
);

KtFile::new("io.example.api")
// FQNs named only from raw body text, which the model cannot see.
.imports(["io.example.api.internal.JNINative".to_string()])
.decl(base)
.decl(describable)
.decl(handler)
.decl(session)
.decl(summary)
.decl(map_replies)
.decl(as_raw)
.decl(KtDecl::TypeAlias {
vis: KtVis::Public,
name: "SampleList".to_string(),
Expand Down Expand Up @@ -343,7 +412,14 @@ fn natives_fragment() -> KtFile {
),
};

let raw_sink = KtFunInterface::new(
"RawSink",
KtFunSig::new("accept").param(KtParam::new("reply", KtType::cls("io.example.api.Reply"))),
)
.vis(KtVis::Internal);

KtFile::new("io.example.api.internal")
.decl(raw_sink)
.decl(natives)
.decl(loader)
// An FQN referenced only from raw text the model cannot see.
Expand Down
27 changes: 24 additions & 3 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ pub struct KtFunSig {
pub kdoc: Option<String>,
/// Generic type-variable names: `["R"]` → `fun <R> …`.
pub generics: Vec<String>,
/// Extension receiver: `Some(Foo)` → `fun Foo.name(…)`. A separate field
/// rather than part of `name`, so `name` stays a plain identifier that can
/// be checked as one.
pub receiver: Option<KtType>,
pub params: Vec<KtParam>,
pub ret: Option<KtType>,
}
Expand All @@ -160,6 +164,7 @@ impl KtFunSig {
annotations: Vec::new(),
kdoc: None,
generics: Vec::new(),
receiver: None,
params: Vec::new(),
ret: None,
}
Expand All @@ -168,6 +173,11 @@ impl KtFunSig {
self.vis = v;
self
}
/// Make this an extension function on `ty`: `fun <R> Foo<R>.name(…)`.
pub fn receiver(mut self, ty: KtType) -> Self {
self.receiver = Some(ty);
self
}
pub fn annotation(mut self, a: impl Into<String>) -> Self {
self.annotations.push(a.into());
self
Expand Down Expand Up @@ -200,6 +210,7 @@ impl From<KtFunSig> for KtFun {
annotations: s.annotations,
kdoc: s.kdoc,
generics: s.generics,
receiver: s.receiver,
params: s.params,
ret: s.ret,
body: KtBody::None,
Expand Down Expand Up @@ -826,6 +837,9 @@ pub struct KtFun {
pub kdoc: Option<String>,
/// Generic type-variable names: `["R"]` → `fun <R> …`.
pub generics: Vec<String>,
/// Extension receiver: `Some(Foo)` → `fun Foo.name(…)`. See
/// [`KtFunSig::receiver`].
pub receiver: Option<KtType>,
pub params: Vec<KtParam>,
pub ret: Option<KtType>,
pub body: KtBody,
Expand All @@ -840,6 +854,7 @@ impl KtFun {
annotations: Vec::new(),
kdoc: None,
generics: Vec::new(),
receiver: None,
params: Vec::new(),
ret: None,
body: KtBody::None,
Expand All @@ -850,6 +865,11 @@ impl KtFun {
self.vis = v;
self
}
/// Make this an extension function on `ty`: `fun <R> Foo<R>.name(…)`.
pub fn receiver(mut self, ty: KtType) -> Self {
self.receiver = Some(ty);
self
}
/// Add a modifier keyword (`override`, `inline`, `operator`, …).
///
/// # Panics
Expand Down Expand Up @@ -903,16 +923,17 @@ impl KtFun {
self
}

/// This function's signature: same name, generics, parameters and return
/// type, with the body and modifiers dropped. What a concrete member looks
/// like as an interface abstract.
/// This function's signature: same name, generics, receiver, parameters and
/// return type, with the body and modifiers dropped. What a concrete member
/// looks like as an interface abstract.
pub fn signature(&self) -> KtFunSig {
KtFunSig {
name: self.name.clone(),
vis: self.vis,
annotations: self.annotations.clone(),
kdoc: self.kdoc.clone(),
generics: self.generics.clone(),
receiver: self.receiver.clone(),
params: self.params.clone(),
ret: self.ret.clone(),
}
Expand Down
10 changes: 10 additions & 0 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ struct SigView<'a> {
/// `external` renders ahead of the other modifiers; it lives on the body.
external: bool,
generics: &'a [String],
/// Extension receiver, rendered as `Recv.` before the name.
receiver: Option<&'a KtType>,
name: &'a str,
params: &'a [KtParam],
ret: Option<&'a KtType>,
Expand All @@ -448,6 +450,7 @@ impl<'a> From<&'a KtFun> for SigView<'a> {
modifiers: &f.modifiers,
external: matches!(f.body, KtBody::External),
generics: &f.generics,
receiver: f.receiver.as_ref(),
name: &f.name,
params: &f.params,
ret: f.ret.as_ref(),
Expand All @@ -464,6 +467,7 @@ impl<'a> From<&'a KtFunSig> for SigView<'a> {
modifiers: &[],
external: false,
generics: &f.generics,
receiver: f.receiver.as_ref(),
name: &f.name,
params: &f.params,
ret: f.ret.as_ref(),
Expand Down Expand Up @@ -494,6 +498,12 @@ fn render_fun_signature(f: &SigView<'_>, level: usize, imports: &mut ImportSet,
if !f.generics.is_empty() {
out.push_str(&format!("<{}> ", f.generics.join(", ")));
}
if let Some(recv) = f.receiver {
// Receiver position, not ordinary type position: a function type needs
// parentheses here (see `KtType::render_receiver`).
out.push_str(&recv.render_receiver(imports));
out.push('.');
}
out.push_str(f.name);
let ps: Vec<String> = f
.params
Expand Down
Loading
Loading