-
Notifications
You must be signed in to change notification settings - Fork 0
Home
H# looks like C# and handles memory like Rust: no garbage collector, nothing leaks,
everything on the heap is freed the moment it stops being used, checked at compile
time, costing nothing at runtime. Source files use the .hs extension and compile
to optimized native executables through LLVM.
You need on your PATH: the .NET 8 SDK (builds the compiler), LLVM 18
(LLVM-C.dll on Windows, libLLVM-18 on Linux/macOS) and clang (links the exe).
dotnet run --project src/HSharp/compiler -- hello.hs # produces hello.exe / hello
./hello.exeThat's the whole workflow: edit, compile, run. Top-level statements are the program,
no main function, no boilerplate.
var x = 10; // int
var pi = 3.5; // float
var flag = true; // bool
var name = "Ann"; // string
var nums = list<int> { 1, 2, 3 };
string label = "explicit"; // optional type annotationint widens to float automatically (1 + 2.5 is 3.5), never the other way.
Numbers become strings on their own when concatenated or interpolated.
Operators: + - * / %, comparisons == != < <= > >=, logic && || ! (short-circuit),
plus ++/-- and += -= *= /= %=. Conditions must be real bools, if (1) won't compile.
Strings compare with == and != only. Comments are // and /* */. Identifiers are case-sensitive.
if (x > 5) { print("big"); }
else if (x == 5) { print("five"); }
else { print("small"); }
var i = 0;
while (i < 3)
{
if (i == 1) { i++; continue; }
print(i);
if (i == 5) { break; }
i++;
}
for (var j = 0; j < 5; j++) { sum += j; }
foreach (var n in nums) { print(n); }break and continue work in all three loop forms, and any values still alive
inside the loop are freed correctly on the way out.
int add(int a, int b)
{
return a + b;
}
string tag(string prefix, int id)
{
return $"{prefix}#{id}";
}
void log(move string line) // 'move' = the function takes ownership
{
print(line);
} // line freed here, at the end of the functionDeclaration order doesn't matter, functions can call each other, mutual recursion works.
return; in a void function is fine. Falling off the end returns a default value.
var fruits = list<string> { "apple", "banana" };
var temps = list<int>(); // empty, typed
fruits.Add("cherry"); // copies the value in
fruits.Remove("banana"); // removes first match, frees it
print(fruits.Count); // 2
print(fruits[0]); // apple
fruits[0] = "pear"; // frees apple, stores pear
foreach (var f in fruits) { print(f); }Indexing is bounds-checked at runtime, a bad index routes to catch (below),
it never reads random memory. Lists own their elements: whatever goes in is copied,
whatever comes out (remove, clear, overwrite) is freed. float and bool lists
aren't in yet.
var s = "hello";
print(len(s)); // 5
var t = s + " world"; // new buffer, s untouched
var same = copy(s); // explicit duplicate
print($"x is {x}, and {x * 2}"); // any expression inside { }
print(contains(s, "ell")); // true
print(startsWith(s, "he")); // true
print(indexOf(s, "llo")); // 2, or -1 when absent
print(sub(s, 1, 3)); // "ell"
print(parseInt("42") + 1); // 43This is the part that makes H# different. int, float, bool copy by value.
string and list have exactly one owner:
var s = "hello";
var t = s; // moves: t owns the buffer now, s is retired
print(t); // fine
// print(s); // compile error: use of moved value 's'
var both = copy(s); // want two? say so. s stays alive.Why: if both stayed valid, one buffer would eventually be freed twice. Instead, ownership transfers at the move and the value is freed at its owner's last use, not at scope end, right after the final line that touches it. You can watch it:
var s = "abc";
print(len(s)); // buffer freed right after this line
print(mem()); // 0, mem() reports live heap allocationsThe rules in short:
- assignment moves,
copy()duplicates, use-after-move is a compile error - moving a variable into a loop body is rejected (it would be freed on iteration one)
- plain function parameters are borrowed: read, print, copy and pass them on,
but you can't
returnthem or hand them to amoveparameter -
moveparameters and return values transfer ownership - a well-formed program ends with
mem() == 0, threads included (the counter is atomic)
var payload = "data";
var t = Task.Run(() =>
{
// runs on the thread pool; payload moved in, plain numbers copied
print(payload);
return 40 + 2;
});
_ = Task.Run(() => { print("fire and forget"); }); // discard form
var answer = await t; // 42Lambdas use () => (or (type name, ...) => when parameters are allowed) and are
allowed wherever a task is expected. Capture rules follow the ownership model:
-
string/listcaptures move the value: after the capture the outer name is gone -
int/float/boolcapture by copy - borrowed values can't be captured,
copy()them first - a task can't assign to the caller's variables
await blocks until the task finishes and hands over its owned result. Inside a
lambda you have the whole language: loops, lists, files, even other tasks.
var ln = Tcp.Listen(8080); // TCP server
var c = ln.Accept(); // waits for a connection
var line = c.Recv(); // reads one line (newline-framed)
c.Send($"echo: {line}\n");
c.Close();
var sock = Tcp.Connect("host", 8080); // TCP client, same Send/Recv/Close
var udp = Udp.Open(); // UDP
udp.SendTo("127.0.0.1", 9000, "ping");
var msg = udp.Recv();Failed network calls (bad host, closed socket, timeouts) route to catch like any
other runtime error. Combine with tasks for a concurrent server: accept in a
Task.Run, handle the connection inside. There's no HTTP builtin yet, but
rt/demo-http.hs in the repo is a complete HTTP/1.1 server and client written
in plain H# using exactly the string functions above, use it as a template.
Runtime failures jump to the nearest catch at the statement boundary. No exception
object, just recovery. Covered today: missing files, out-of-bounds indexes,
division by zero, failed network calls:
try
{
var data = read("config.txt");
var x = 10 / n;
}
catch
{
print("something failed, using defaults");
}One gap to know: an error inside a task does not propagate to the caller yet, the task just returns a default value.
| call | does |
|---|---|
print(v) |
print any value with a newline |
input(prompt) |
read one line from the user, returns string |
len(s) / len(list)
|
length |
copy(s) |
duplicate a string |
contains(s, sub) |
substring test, bool |
startsWith(s, prefix) |
prefix test, bool |
indexOf(s, sub) |
first position, or -1 |
sub(s, start, len) |
substring as a new string |
parseInt(s) |
string to int |
read(path) |
whole file as a string (routes to catch if missing) |
write(path, content) |
write file to disk |
exists(path) |
true if the file exists |
delete(path) |
remove a file |
mem() |
live heap allocation count, should be 0 at exit |
hsc program.hs # native build for the OS you're on
hsc program.hs -o myname # pick the output name
hsc program.hs -platform linux64 # cross-compile: win64, linux64, osx64,
# linux-arm64, osx-arm64
Every build runs LLVM's O2 optimization pipeline automatically.
Extra flags pass through to clang, e.g. hsc app.hs -platform linux64 --sysroot=C:\sys\linux64
(cross-linking needs the target's C library, codegen works from anywhere).
Errors come with positions: app.hs(14,7): error: use of moved value 's'.
Cooperative async (await frees the thread instead of blocking; planned next),
HTTPS/TLS, HTTP as builtins), float/bool lists,
list deep-copy, multi-file programs. Set HS_DUMP_IR=1 to dump the generated
IR on every build while debugging.