I just want to point out that there's a sort of "syntax error" in your
proposal.
Java provides annotations as a means of "structured comments" on
declarations and type uses, but the Java language does not, and will
not, impart any semantics to programs on the basis of annotations. If
you are talking about writing a static analysis tool, perhaps a
pluggable checker in the Checkers framework, then (as Ethan points out)
you can use existing annotations with APs and do so, and the compiler is
merely a conduit for ferrying the annotations to where an AP can find
them. (In fact, there is already a checker for "fake enums", where you
say that a given `int` is really one of the enumerated set 1, 2, 3, 4,
which is a restriction type.)
If you mean that the compiler actually is going to get into the act,
though, then this is not an annotation-driven feature, this is a
full-blown language feature and should be thought of accordingly. I
know its tempting to view annos as a "shortcut" to language features,
but if something has semantics, its part of the language, and sadly that
means no shortcuts. That's not to say it isn't a worthwhile idea with a
good cost-to-benefit ratio. (Indeed, as we get further into the type
classes work, the logic of a `newtype` mechanism becomes even more
compelling as then it becomes possible to affect behavior with
restrictions such as `CaseInsensitveString`, which doesn't actually
restrict the value set of the type, but allows you to define `Ord
CaseInsentiveString` separate from `Ord String`.)
On 10/13/2025 3:17 PM, Archie Cobbs wrote:
Ethan McCue <[email protected]> wrote:
However there is nothing conceptually preventing the tools
validating @NonNull usage from also emitting an error until you
have inserted a known precheck.
...
But for other single-value invariants, like your @PhoneNumber
example, it seems fairly practical. Especially since, as a general
rule, arbitrary cost computations really shouldn't be invisible.
How would one know if (@B A) is going to thread invocations of
some validation method everywhere?
This is why 3rd party tools aren't as good as having the compiler
handle it, because the compiler is in a position to provide both
stronger and more efficient guarantees - think generic types and
runtime erasure. Compiler-supported typing allows the developer
to move the burden of proof from the method receiving a parameter to
the code invoking that method, and onward back up the call chain, so
that validations tend to occur "early", when they are first known to
be true, instead of "late" at the (many more) points in the code where
someone actually cares that they are true.
So if phone numbers are central to your application, and they are
passed around and used all over the place as type@PhoneNumber String,
then they will only need to actually be validated at a few application
entry points, not at the start of every method that has a phone number
as a parameter. In other words, the annotation is ideally not a
"to-do" list but rather an "it's already done" list.
The guarantee that the compiler would then provide is ideally on the
same level as with generics: while it's being provided by the
compiler, not the JVM, so you can always get around it if you try hard
enough (native code, reflection, class file switcheroo, etc.), as long
as you "follow the rules" you get the guarantee - or if not, an error
or at least a warning.
Brian Goetz <[email protected]> wrote:
I think the best bet for making this usable would be some
mechanism like a "view", likely only on value types, that would
erase down to the underlying wrapped type, but interpose yourself
on construction, and provided a conversion from T to RefinedT that
verified the requirement. But this is both nontrivial and
presumes a lot of stuff we don't even have yet...
I think that is close to what I was imagining. It seems like it could
be done with fairly minimal impact/disruption...? No need for wrappers
or views.
But first just to be clear, what I'm getting at here is a fairly
narrow idea, i.e., what relatively simple thing might the compiler do,
with a worthwhile cost/benefit ratio, to make it easier for developers
to reason about the correctness of their code when "type restriction"
is being used, either formally or informally (meaning, if you're using
an int to pass around the size of collection, you're doing informal
type restriction).
What's the benefit? Type restriction is fairly pervasive, and yet
because Java doesn't make it very easy to do, it's often not being
done at all, and this ends up adding to the amount of manual work
developers must do to prove to themselves their code is correct. The
more of this burden the compiler could take on, the bigger the benefit
would be.
What's the cost? That depends on the solution of course.
To me the giant poster-child for this kind of pragmatic language
addition is generics. It had all kinds of minor flaws from the point
of view of language design, but the problem it addressed was so
pervasive, and the new tool it provided to developers for verifying
the correctness of their code was so powerful, that nobody thinks it
wasn't worth the trade-off.
OK let me throw out two straw-man proposals. I'll just assume these
are stupid/naive ideas with major flaws. Hopefully they can at least
help map out the usable territory - if any exists.
*Proposal #1*
This one is very simple, but provides a weaker guarantee.
1. The compiler recognizes and tracks "type restriction annotations",
which are type annotations having themeta-annotation @TypeRestriction
2. For all operations assigning some value v of type S to type T:
1. If a type restriction annotation A is present on T but not S,
the compiler generates a warning in the new lint category
"type-restriction"
That's it. A cast likevar pn = (@PhoneNumber String)input functions
simply as a developer assertion that the type restriction has been
verified, but the compiler does not actually check this. There is no
change to the generated bytecode. If the developer chooses to write a
validation method that takes a string, validates it (or throws an
exception), and then returns the validated string, that method will
need to be annotated with@SuppressWarnings("type-restriction") because
of the cast in front of the return statement.
Guarantee provided: Proper type restriction as long as
"type-restriction" warnings are enabled and not emitted. However, this
is a "fail slow" guarantee: it's easy to defeat (just cast!). So if
you write a method that takes a@PhoneNumber String parameter that is
passed an invalid value, you won't find out until something goes wrong
later down the line (or never). In other words, /your/ code will be
correct, but you have to be trusting of any code that /invokes/ your
code, which in practice is not always a sound strategy.
*Proposal #2*
This is proposal is more complex but provides a stronger guarantee:
1. The compiler recognizes and tracks "type restriction annotations",
which have themeta-annotation @TypeRestriction
1. The annotation specifies a user-supplied "constructor" class
providing a user-defined construction/validation method
validate(v)
2. We add class TypeRestrictionException extends
RuntimeException and encourage validate() methods to throw
(some subclass of) it
2. For all operations assigning some value v of type S to type T:
1. If a type restriction annotation A is present on T but not S,
the compiler generates a "type-restriction" warning AND adds
an implicit cast added (see next step)
3. For every cast like var pn = (@PhoneNumber String)"+15105551212"
the compiler inserts bytecode to invoke the appropriate
enforcervalidate(v) method
4. The JLS rules for method resolution, type inference, etc., do not
change (that would be way over-complicating things)
1. Two methodsvoid dial(String pn) andvoid dial(@PhoneNumber
String pn) will still collide
Guarantee provided: Proper type restriction unless you are going to
extremes (native code, reflection, runtime classfile switcheroo,
etc.). This is a "fail fast" guarantee: errors are caught at the
moment an invalid value is assigned to a type-restricted variable. If
your method parameters have the annotation, you don't have to trust
3rd party code that calls those methods (as long as it was compiled
properly). I.e., same level of guarantee as generics.
These are by no means complete or particularly elegant solutions from
a language design point of view. They are pragmatic and relatively
unobtrusive add-ons, using existing language concepts, to get us most
of what we want, which is:
* User-defined "custom" type restrictions with compile-time
checking/enforcement
o As with generics, the goal is not language perfection, but
rather making it easier for developers to reason about correctness
* Compile-time guarantees that type restricted values in source
files will be actually type restricted at runtime
* Efficient implementation
o Validation only happens "when necessary"
o No JVM changes needed (erasure)
* No changes to language syntax; existing source files are 100%
backward compatible
The developer side of me says that the cost/benefit ratio of something
like this would be worthwhile, in spite of its pragmatic nature,
simply because the problem being addressed seems so pervasive. I felt
the same way about generics (which was a much bigger change addressing
a much bigger pervasive problem).
But I'm sure there are things I'm missing... ?
-Archie
--
Archie L. Cobbs