About V#
V-Sharp: the non-object-oriented subset of C# 13, compiled to standard JVM class files.
Purpose
V# takes the part of C# that does not need an object model and compiles it to ordinary JVM
bytecode. You write C# — the literals, the patterns, the interpolation, the tuples,
async/await — and the output is a normal .class
file that any Java program can call, any JVM can run, and javap can read.
There is no runtime bridge, no shim layer and no reimplementation of the JDK.
java.util.ArrayList is the list you use, and
Task<T> is java.util.concurrent.Future<T>. The
language does not wrap the platform it runs on; it addresses it directly.
Design philosophy
-
No dependencies. The compiler is written in pure Java 25 against the JDK
only. No ASM, no ANTLR, no Jackson, no JUnit. Bytecode is emitted with
java.lang.classfile, the parser is hand-written, and the test harness lives in the repository. In the project's own words: “The dependency list is empty, and it is meant to stay that way.” - One program, one spelling. V# is stricter than C# in exactly three places, and each is a compiler error rather than a convention. See the three rules below.
- Exclusions are decisions, not gaps. Every excluded C# feature has a stated semantic reason, and none of them is a “not yet”. See what is deliberately not here.
- Determinism. Two compilations of the same sources produce byte-identical artifacts.
Technical background
The compiler is a conventional, fully self-contained pipeline:
V# source → lexer → parser → AST → semantic analysis → typed IR → lowering → JVM bytecode
Numeric and string rendering is culture-free and verified digit-for-digit against .NET 10.
checked arithmetic, C#'s exception types and try/catch/finally
are implemented over real JVM exception tables, and JDK exceptions can be caught and thrown
directly.
Relationship with the JVM and the Java ecosystem
This is the part that makes V# a JVM language rather than a language that targets the JVM.
JDK types are resolved against the JDK the compiler runs on, generics cross the boundary, and
the whole default module image is nameable — java.sql,
java.net.http, java.util.logging — with no configuration.
Using the JDK from V#
using System;
using java.lang;
using java.util;
using java.time;
List<string> names = new ArrayList<string>();
names.Add("grace");
names.Add("ada");
Collections.Sort(names);
Map<string, int> lengths = new HashMap<string, int>();
foreach (string name in names)
{
lengths.Put(name, name.Length);
}
Duration window = Duration.OfMinutes(90);
Console.WriteLine($"{names.Get(0)}|{lengths.Get("grace")}|{window.ToHours()}h|{Integer.MAX_VALUE}");
// ada|5|1h|2147483647
Using V# from Java
It is just bytecode:
namespace Demo;
public static class Greet
{
public static string Hello(string name)
{
return $"hello, {name}";
}
}
import Demo.Greet;
public final class FromJava {
public static void main(String[] args) {
System.out.println(Greet.Hello("java")); // hello, java
}
}
$ javap -cp out Demo.Greet
public class Demo.Greet {
public Demo.Greet();
public static java.lang.String Hello(java.lang.String);
}
Three rules that will surprise you
V# is stricter than C# in exactly three places. All three exist so that one program has one spelling.
1. Allman braces are grammar
Every block brace stands alone on its line, indented four spaces per enclosing brace pair.
static int Add(int a, int b) { // error VS20007: Opening brace must appear on its
return a + b; // own new line (Allman style)
}
Array initializers, property subpatterns and with initializers may be written fully
inline ({ 1, 2, 3 }) or fully Allman — mixing the two reports
VS20008 as well. Tabs are not indentation.
2. JVM members are written in PascalCase
The compiler owns the translation, and it touches the first character only.
file.toURI(); // error VS20006: Java member 'toURI' must be written as 'ToURI'
file.ToURI(); // correct
Integer.MAX_VALUE keeps its own spelling — a name with no lowercase letter is
not PascalCase, so it is matched verbatim. Integer.MaxValue is an error.
3. Types are imported, never qualified
java.util.ArrayList<int> x = new java.util.ArrayList<int>();
// error VS20012: Name 'ArrayList' by its simple name and import it with 'using java.util;'
A qualified name belongs in a using directive, an alias target, or a
using static target — nowhere else. Nested names like
Map.Entry<string, int> are not qualified names and stay legal.
What is deliberately not here
V# is the subset of C# that does not need the object model. These are excluded by design, each for a stated semantic reason, and none of them is a “not yet”:
| Excluded | Why |
|---|---|
Classes with instance state, inheritance, virtual/override, interfaces you declare |
The feature is the object model |
User-declared delegate types, multicast, events |
A delegate is a reference type with instance state and an invocation list |
| LINQ query expressions | Built on delegates and extension-method chains over an object model |
Iterators (yield return) |
Compiles to a generated state-machine class |
| Anonymous types, object initializers on classes | No user classes to initialize |
unsafe, pointers, stackalloc, fixed, ref struct, Span<T> |
No unmanaged memory model on the JVM |
What you get instead: values, enums, struct, record struct, tuples,
static containers, free functions, lambdas, and the whole JDK. Func and
Action are available — they denote JDK functional interfaces
(Supplier/Function/BiFunction,
Runnable/Consumer/BiConsumer) by arity, so
Func<int, int> twice = x => x * 2; compiles and twice(21)
lowers to the interface's single abstract method.
The full classification — every C# 13 construct, its status, its JVM lowering and its
tests — is in
docs/FEATURE-MATRIX.md.
