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

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”:

C# features excluded from V#, and the reason for each
ExcludedWhy
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.