From C#​

Much of JaiScript's surface is familiar from C#: classes with : this(...) chaining, a required override, statics, accessor properties, try/catch/throw, arrow lambdas, namespaces, ?. and ??, and yield.

The semantics underneath are C++'s. Three things to learn first:

  1. A class is a value, as a C# struct is: b = a copies the object. new Hero() makes the shared reference that every C# class is, a shared_ptr<Hero>.
  2. A class value is never null. Hero h = null; is a load error; a slot that can be empty is a shared_ptr<Hero> or a var.
  3. var is C#'s dynamic; auto is C#'s var.

Ported side by side​

Five small C# programs and their JaiScript ports. Each pair was run, with .NET 10 and on both JaiScript backends, and printed the output shown. A JaiScript function is declared above the top-level code that calls it.

A class as a reference type​

c#
var party = new List<Hero> { new Hero("Ada", 30), new Hero("Brom", 22) };
Hero leader = party[0];                      // a second reference to Ada
leader.Hp -= 10;
foreach (var h in party) { h.Hp += 1; }
Hero? target = null;                         // a reference may be empty
target ??= party[1];
Heal(target, 5);
string same = leader == party[0] ? "one Ada" : "two Adas";
Console.WriteLine($"{party[0].Name} {party[0].Hp}, {party[1].Name} {party[1].Hp}, {same}");

static void Heal(Hero hero, int amount) { hero.Hp += amount; }

class Hero {
    public string Name;
    public int Hp;
    public Hero(string name, int hp) { Name = name; Hp = hp; }
}
jaiscript
class Hero {
    string name;
    int hp;
    Hero(string n, int h) { name = n; hp = h; }
}
void heal(shared_ptr<Hero> hero, int amount) { hero.hp += amount; }

array<shared_ptr<Hero>> party = [new Hero("Ada", 30), new Hero("Brom", 22)];
shared_ptr<Hero> leader = party[0];          // a second handle to Ada
leader.hp -= 10;
for (auto h : party) { h.hp += 1; }
shared_ptr<Hero> target = null;              // a handle may be empty
target ??= party[1];
heal(target, 5);
string same = leader == party[0] ? "one Ada" : "two Adas";
print(`${party[0].name} ${party[0].hp}, ${party[1].name} ${party[1].hp}, ${same}`);
output
Ada 21, Brom 28, one Ada
  • Every C# class variable is a reference. JaiScript spells that out: new Hero(...) makes a shared_ptr<Hero>, and leader, target and the elements are handles to it. Only a handle can be null, and == on two handles asks whether they are one object.
  • Without new and the handle type the same lines work on values that copy: a class is a value has that program.

A LINQ pipeline​

c#
var units = new List<Unit> { new("ogre", 30), new("imp", 8), new("elf", 15) };
List<string> names = units.Where(u => u.Hp > 10).OrderBy(u => u.Hp).Select(u => u.Name).ToList();
int total = units.Sum(u => u.Hp);
Unit? giant = units.FirstOrDefault(u => u.Hp > 99);
units.Sort((a, b) => b.Hp.CompareTo(a.Hp));  // in place
units.RemoveAll(u => u.Hp < 10);
Console.WriteLine($"{string.Join(", ", names)} | {total} | {giant?.Name ?? "nobody"} | {units[0].Name} of {units.Count}");

record Unit(string Name, int Hp);
jaiscript
class Unit { string name; int hp; }

array<Unit> units = [Unit("ogre", 30), Unit("imp", 8), Unit("elf", 15)];
auto healthy = array::sort(array::filter(units, u => u.hp > 10), (a, b) => a.hp < b.hp);
array<string> names = array::map(healthy, u => u.name);
int total = units.reduce((sum, u) => sum + u.hp, 0);
var giant = units.find(u => u.hp > 99);
units.sort((a, b) => a.hp > b.hp);           // in place
units.remove_if(u => u.hp < 10);
print(`${names.join(", ")} | ${total} | ${giant?.name ?? "nobody"} | ${units[0].name} of ${units.size()}`);
output
elf, ogre | 53 | nobody | ogre of 2
  • The pipeline is the array:: functions, which return new arrays; units.sort and units.remove_if work in place.
  • A comparator answers "does a go first?" with a bool. find answers null when nothing matches. A class with no constructor fills its fields in order, as a positional record does.

A dictionary​

c#
var stock = new Dictionary<string, int> { ["arrow"] = 20, ["potion"] = 3 };
stock["rope"] = 1;
stock["arrow"] -= 5;
if (stock.TryGetValue("potion", out int potions)) { Console.WriteLine($"potions: {potions}"); }
int swords = stock.GetValueOrDefault("sword", 0);
try {
    Console.WriteLine(stock["sword"]);
} catch (KeyNotFoundException) {
    Console.WriteLine($"no sword; default {swords}");
}

var counts = new SortedDictionary<string, int>();
foreach (string word in "the cat and the hat".Split(' ')) {
    counts[word] = counts.GetValueOrDefault(word) + 1;
}
var parts = new List<string>();
foreach (var (word, count) in counts) { parts.Add($"{word}={count}"); }
Console.WriteLine(string.Join(" ", parts));
jaiscript
map<string, int> stock = {"arrow": 20, "potion": 3};
stock["rope"] = 1;
stock["arrow"] -= 5;
if (stock.contains("potion")) { print(`potions: ${stock["potion"]}`); }
int swords = stock.get("sword", 0);
try {
    print(stock["sword"]);
} catch (error e) {
    print(`no sword; default ${swords}`);
}

map<string, int> counts;
for (string word : "the cat and the hat".split(" ")) {
    counts[word] = counts.get(word, 0) + 1;
}
array<string> parts;
for (auto [word, count] : counts) { parts.push(`${word}=${count}`); }
print(parts.join(" "));
output
potions: 3
no sword; default 0
and=1 cat=1 hat=1 the=2
  • m[k] for a missing key raises a run-time error, as the C# indexer throws. get(k, fallback) is GetValueOrDefault, and contains before m[k] replaces TryGetValue.
  • A map iterates in key order, as a SortedDictionary does.

An abstract base and a list of them​

c#
var shapes = new List<Shape> { new Circle(1.0), new Rect(2.0, 3.5) };
double total = 0.0;
foreach (Shape s in shapes) {
    Console.WriteLine(s);
    total += s.Area();
}
Console.WriteLine($"total {total:F2}");

abstract class Shape {
    private readonly string label;
    protected Shape(string label) { this.label = label; }
    public abstract double Area();
    public virtual string Name() => "shape";
    public override string ToString() => $"{label} {Name()}, area {Area():F2}";
}
class Circle : Shape {
    private readonly double radius;
    public Circle(double r) : base("round") { radius = r; }
    public override double Area() => Math.PI * radius * radius;
    public override string Name() => "circle";
}
class Rect : Shape {
    private readonly double width, height;
    public Rect(double w, double h) : base("boxy") { width = w; height = h; }
    public override double Area() => width * height;
}
jaiscript
class Shape {
private:
    string label;
public:
    Shape(string l) { label = l; }
    float area() { throw "Shape.area is abstract"; }
    string name() { return "shape"; }
    string to_string() { return `${label} ${name()}, area ${area():.2f}`; }
}
class Circle : Shape {
private:
    float radius;
public:
    Circle(float r) : super("round") { radius = r; }
    float area() override { return math::PI * radius * radius; }
    string name() override { return "circle"; }
}
class Rect : Shape {
private:
    float width;
    float height;
public:
    Rect(float w, float h) : super("boxy") { width = w; height = h; }
    float area() override { return width * height; }
}

array<shared_ptr<Shape>> shapes = [new Circle(1.0), new Rect(2.0, 3.5)];
float total = 0.0;
for (auto s : shapes) {
    print(s);
    total += s.area();
}
print(`total ${total:.2f}`);
output
round circle, area 3.14
boxy shape, area 7.00
total 10.14
  • No abstract and no virtual: the base method throws, every method is virtual, and override is required. : base(...) is : super(...), and access is set by private: and public: sections.
  • to_string() is ToString(): print and ${...} call it.

A custom exception and using​

c#
foreach (int slot in new[] { 1, 7 }) {
    try {
        Save(slot);
    } catch (SaveFailed e) {
        Console.WriteLine($"failed: {e.Message} (slot {e.Slot})");
    }
}

static void Save(int slot) {
    using var guard = new SlotLock(slot);
    if (slot > 3) { throw new SaveFailed(slot, "no such slot"); }
    Console.WriteLine($"saved {slot}");
}

class SaveFailed : Exception {
    public int Slot { get; }
    public SaveFailed(int slot, string why) : base(why) { Slot = slot; }
}
class SlotLock : IDisposable {
    private readonly int slot;
    public SlotLock(int slot) { this.slot = slot; Console.WriteLine($"lock {slot}"); }
    public void Dispose() { Console.WriteLine($"unlock {slot}"); }
}
jaiscript
class SaveFailed {
    int slot;
    string message;
}
class SlotLock {
private:
    int slot;
public:
    SlotLock(int s) { slot = s; print(`lock ${slot}`); }
    ~SlotLock() { print(`unlock ${slot}`); }
}

void save(int slot) {
    SlotLock guard(slot);
    if (slot > 3) { throw SaveFailed(slot, "no such slot"); }
    print(`saved ${slot}`);
}

for (int slot : [1, 7]) {
    try {
        save(slot);
    } catch (SaveFailed e) {
        print(`failed: ${e.message} (slot ${e.slot})`);
    }
}
output
lock 1
saved 1
unlock 1
lock 7
unlock 7
failed: no such slot (slot 7)
  • Any value can be thrown, so SaveFailed extends nothing, and catch (SaveFailed e) selects by type.
  • using and IDisposable become a destructor: guard is a value, and ~SlotLock() runs when save ends, by return or by throw.

Feature by feature​

Variables and types​

C#JaiScript
inferred, dynamicvar n = 5;, dynamic x = 5;auto n = 5;, var x = 5;
numbersint (32-bit), long, doubleint, 64-bit and always checked; float (or double), 64-bit
string, char, boolthe same namesthe same names; a string is a value of bytes
constantconst, readonlyconst int MAX = 10;; no readonly
nullnull, int?null, held by a var, a shared_ptr<T> or a function; never by an int or a class value
narrowingint i = (int)3.9;int i = cast<int>(3.9);; a float never enters an int implicitly
collectionsnew List<int> { 1, 2 }, new Dictionary<string, int> { ["a"] = 1 }array<int> xs = [1, 2];, map<string, int> m = {"a": 1};
type namex.GetType().Nametype_of(x)

Collections and LINQ​

The array:: and map:: functions return a new container, like LINQ; the dot methods of the same name change the container in place and return nothing, like List<T>.Sort. Nothing is deferred: each step runs when called.

C#JaiScript
list.Add(x), list.Count, list.Insert(i, x)xs.push(x), xs.size(), xs.insert(i, x)
Contains(x), IndexOf(x)xs.contains(x), xs.index_of(x)
RemoveAt(i), RemoveAll(p)xs.remove(i), xs.remove_if(p)
foreach (var x in xs)for (auto x : xs) copies each element; for (auto& x : xs) names it. No foreach keyword
xs.Where(p), xs.Select(f)array::filter(xs, p), array::map(xs, f)
xs.OrderBy(u => u.Hp)array::sort(xs, (a, b) => a.hp < b.hp)
xs.Take(n), xs.Concat(ys)array::slice(xs, 0, n), array::concat(xs, ys)
xs.Sum(), xs.Any(p), xs.All(p)xs.reduce((sum, x) => sum + x, 0), xs.some(p), xs.every(p)
xs.FirstOrDefault(p)xs.find(p): null when none matches. There is no throwing First(p)
FindIndex(p), ForEach(f)xs.find_index(p) (-1 when none), xs.for_each(f)
string.Join(", ", xs)xs.join(", ")
d[k], d.ContainsKey(k), d.Remove(k), d.Keysm[k] (a missing key raises), m.contains(k), m.erase(k), m.keys() in key order
d.GetValueOrDefault(k, fallback)m.get(k, fallback). For TryGetValue, test m.contains(k) first
foreach (var kv in d)for (auto [k, v] : m)

Classes​

C#JaiScript
class, structa class is a reference type, a struct a value typeevery class is a value type; no struct keyword. new Hero() makes the shared reference
chaining: this(0, 0), : base(5), base.Describe(): this(0, 0), : super(5), super::describe()
virtual, overridevirtual in the base, override in the subclassevery method is virtual; override is required
abstract, interfaceabstract class Shape, interface IShapeno such keywords. For abstract, a base class whose methods have default bodies (or throw). For an interface, a var or auto& parameter: it takes any object, and a missing method raises when called
accessa modifier on each memberpublic: and private: section labels, as in C++ (private int x; does not parse). private admits subclasses, as C#'s protected does, and protected: means the same
propertypublic int Hp { get; set; }a field, or accessor methods int _get_hp() and void _set_hp(int v) that obj.hp goes through
staticHero.CountHero::count; Hero.count and hero.count are load errors that name it
ToStringoverride string ToString()string to_string(), used by print(x), to_string(x), `${x}` and "text" + x
equalityreference equality by default== compares fields; operator== replaces it; same_object(a, b) is identity
operatorspublic static Vec operator+(Vec a, Vec b)Vec operator+(Vec o), a method
asx as Herocast<Hero&>(x), which raises a catchable error when x is not a Hero
isx is Herox instanceof Hero: a Hero or a class derived from it. type_of(x) == "Hero" is the exact class only
using, IDisposable, finallyusing (var f = Open()), finally { }a destructor, ~File(), runs when its scope ends, a throw included; no finally
c#
Paladin p = new Paladin("Ada");
p.Hp = -3;                                   // through the setter
object anyone = p;
object text = "text";
Console.WriteLine($"{p.Describe()} {p.Hp} {Hero.Count} hero: {p}");
Console.WriteLine($"{anyone.GetType().Name == "Hero"} {anyone is Hero} {text is Hero}");

class Hero {
    private int hp = 10;
    public static int Count = 0;
    public string Name;
    public Hero(string name) { Name = name; Count += 1; }
    public int Hp { get => hp; set => hp = value < 0 ? 0 : value; }
    public virtual string Describe() => Name;
    public override string ToString() => $"{Name} ({hp} hp)";
}
class Paladin : Hero {
    public Paladin(string name) : base(name) { }
    public override string Describe() => "Sir " + base.Describe();
}
output
Sir Ada 0 1 hero: Ada (0 hp)
False True False
jaiscript
class Hero {
private:
    int hp_value = 10;
public:
    static int count = 0;
    string name;
    Hero(string n) { name = n; Hero::count += 1; }
    int _get_hp() { return hp_value; }
    void _set_hp(int v) { hp_value = v < 0 ? 0 : v; }
    string describe() { return name; }
    string to_string() { return `${name} (${hp_value} hp)`; }
}
class Paladin : Hero {
    Paladin(string n) : super(n) {}
    string describe() override { return "Sir " + super::describe(); }
}

Paladin p = Paladin("Ada");
p.hp = -3;                                   // through _set_hp
var anyone = p;
var text = "text";
print(p.describe(), " ", p.hp, " ", Hero::count, " hero: " + p);
print(type_of(anyone) == "Hero", " ", anyone instanceof Hero, " ", text instanceof Hero);
try { throw anyone; } catch (Hero h) { print("caught a Hero: ", h.name); }
output
Sir Ada 0 1 hero: Ada (0 hp)
false true false
caught a Hero: Ada

C# prints its bools capitalized and throws only an Exception, so its program has no third line.

The is-a test. x instanceof Hero is true for a Hero or a subclass and false for anything else, a value that is no object included. After it, cast<Hero&>(x) cannot fail; as stays the cast, which raises where C# would give null. A typed catch (Hero h) takes a Hero or a subclass the same way.

Errors, strings and the rest​

C#JaiScript
throwthrow new InvalidOperationException("x");throw "x";: any value
catchcatch (IOException e) { e.Message; }catch (IoError e) by type; catch (error e) for a run-time error (e.message, e.kind, e.stack); catch (e) for anything
interpolation, format$"{name} has {hp:F1}", string.Format("{0}", x)`${name} has ${hp:.1f}`, format("{}", x)
concatenation"hp: " + 5a load error: + joins a string with a string, a char or an object that has to_string(). Write "hp: " + to_string(5) or `hp: ${5}`
string methodsreturn new stringss.to_upper(), s.trim(), s.replace(a, b) change s and return nothing; string::to_upper(s) returns a new string
parseint.TryParse(s, out n)s.to_int(): null when the text is not a number; s.to_int(0) supplies the fallback
null operatorsx?.Name, x ?? y, x ??= ythe same three
switchevery case ends in breakbreaks by default; empty stacked labels share a body, and fallthrough; replaces goto case
checkedchecked { }always: integer overflow raises, and math::wrap_add wraps on purpose
lambdax => x * 2x => x * 2, (int x) => x * 2, or C++'s [](int x) { return x * 2; }
delegate, eventFunc<int, int>, event Action Clickeda function slot (any callable, no signature); an array<function> of handlers
generic methodT Max<T>(T a, T b)auto biggest(auto a, auto b)
iteratorIEnumerable<int> Evens() { yield return 0; }coroutine int evens() { yield 0; }, driven by range-for or next()
async, awaitawait Task.Delay(100)none: a coroutine the host advances each frame; each next() runs to the next yield
namespacenamespace Game { }, using Game;namespace game { } and game::f(); using game::f; brings in one name
JSONJsonSerializer.Serialize(o)to_json(v), from_json(s)
c#
var seen = new List<int>();
foreach (int e in Evens(7)) { seen.Add(e); }

var b = new Button();
int clicks = 0;
b.Clicked += () => clicks += 1;
b.Click();
b.Click();

string name = "  Ada ";
name = name.Trim();                          // returns a new string
double hp = 7.25;
int.TryParse("abc", out int parsed);         // false, and parsed is 0
Console.WriteLine($"[{string.Join(",", seen)}] {clicks} {name} has {hp:F1} {int.Parse("42") + 1} {parsed}");

static IEnumerable<int> Evens(int limit) {
    for (int i = 0; i < limit; i += 2) { yield return i; }
}

class Button {
    public event Action? Clicked;
    public void Click() { Clicked?.Invoke(); }
}
jaiscript
coroutine int evens(int limit) {
    for (int i = 0; i < limit; i += 2) { yield i; }
}
array<int> seen = [];
for (int e : evens(7)) { seen.push(e); }

class Button {
    array<function> clicked = [];
    void click() { for (auto& handler : clicked) { handler(); } }
}
Button b;
int clicks = 0;
b.clicked.push([]() { clicks += 1; });
b.click();
b.click();

string name = "  Ada ";
name.trim();                                 // changes name
float hp = 7.25;
print(to_json(seen), " ", clicks, " ", `${name} has ${hp:.1f}`, " ", "42".to_int() + 1, " ", "abc".to_int(0));
output
[0,2,4,6] 2 Ada has 7.2 43 0

Watch out​

A class is a value, and so is a list of them​

Assignment, a by-value parameter and a by-value return copy the whole object. An array<Unit> holds the units themselves, so a loop variable without & is a copy. & is C#'s ref, without the keyword at the call. An array of handles behaves like a C# List<Unit>. Value Semantics & References is the chapter on this.

c#
Unit a = new Unit();
Unit b = a;                                  // a second reference to one Unit
b.Hp = 50;
Hit(a);
Console.WriteLine($"{a.Hp} {b.Hp}");

var units = new List<Unit> { new Unit(), new Unit() };
foreach (var u in units) { u.Hp -= 5; }      // u refers to the element
Console.WriteLine(units[0].Hp);

static void Hit(Unit u) { u.Hp -= 1; Console.WriteLine($"inside: {u.Hp}"); }   // the caller's Unit

class Unit { public int Hp = 10; }
output
inside: 49
49 49
5
jaiscript
class Unit { int hp = 10; }
Unit a;
Unit b = a;                                  // a copy
b.hp = 50;
void hit(Unit u) { u.hp -= 1; print("inside: ", u.hp); }   // gets a copy
void hit_ref(Unit& u) { u.hp -= 1; }                        // like ref Unit
hit(a);
hit_ref(a);
print(a.hp, " ", b.hp);

array<Unit> units = [Unit(), Unit()];
for (auto& u : units) { u.hp -= 5; }         // u is the element
array<shared_ptr<Unit>> handles = [new Unit()];
for (auto h : handles) { h.hp = 1; }         // a copy of a handle shares the object
print(units[0].hp, " ", handles[0].hp);
output
inside: 9
9 50
5 1

In C#, a, b and the parameter reach one Unit, so every number moves together; JaiScript copies at b = a and at the call.

A write to a copy that nothing reads afterwards is usually a mistake, and the plainest cases fail the load (lost writes):

c#
var units = new List<Unit> { new Unit(), new Unit() };
foreach (var u in units) { u.Hp -= 5; }      // changes the elements
Unit? nobody = null;
Console.WriteLine($"{units[0].Hp} {nobody == null}");

class Unit { public int Hp = 10; }
output
5 True
jaiscript
class Unit { int hp = 10; }
array<Unit> units = [Unit(), Unit()];
for (auto u : units) { u.hp -= 5; }
Unit nobody = null;
output
jaiscript: static check failed:
<script>:3:24: error: 'u.hp' writes to 'u', a copy of the element that is never read afterwards, so the write is lost; iterate with 'auto& u' to change the element
    for (auto u : units) { u.hp -= 5; }
                           ^
<script>:4:6: error: cannot initialize 'nobody' declared 'Unit' with 'null': a 'Unit' is never null; declare 'shared_ptr<Unit>' (or var) for a slot that can be empty
    Unit nobody = null;
         ^

var is dynamic; auto is C#'s var​

A var takes anything for its whole life. An auto takes its type from the initializer and keeps it: auto n = 5; n = "text"; is the load error cannot assign 'string' to 'n' declared 'int'.

Integers are 64-bit and checked​

2147483647 + 1 is 2147483648. Overflow at 263 raises Integer overflow in '+', as if every block were checked.

Where to next​

Value Semantics & References first. Then the Type Ladder for var, auto and typed slots, Classes for constructors, accessors and access, and Coroutines for what replaces async. The ten-minute tour builds a small game with all of it.