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:
- A class is a value, as a C#
structis:b = acopies the object.new Hero()makes the shared reference that every C# class is, ashared_ptr<Hero>. - A class value is never null.
Hero h = null;is a load error; a slot that can be empty is ashared_ptr<Hero>or avar. varis C#'sdynamic;autois C#'svar.
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
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; }
}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}`);Ada 21, Brom 28, one Ada- Every C# class variable is a reference. JaiScript spells that out:
new Hero(...)makes ashared_ptr<Hero>, andleader,targetand the elements are handles to it. Only a handle can be null, and==on two handles asks whether they are one object. - Without
newand the handle type the same lines work on values that copy: a class is a value has that program.
A LINQ pipeline
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);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()}`);elf, ogre | 53 | nobody | ogre of 2- The pipeline is the
array::functions, which return new arrays;units.sortandunits.remove_ifwork in place. - A comparator answers "does a go first?" with a bool.
findanswers null when nothing matches. A class with no constructor fills its fields in order, as a positional record does.
A dictionary
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));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(" "));potions: 3
no sword; default 0
and=1 cat=1 hat=1 the=2m[k]for a missing key raises a run-time error, as the C# indexer throws.get(k, fallback)isGetValueOrDefault, andcontainsbeforem[k]replacesTryGetValue.- A
mapiterates in key order, as aSortedDictionarydoes.
An abstract base and a list of them
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;
}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}`);round circle, area 3.14
boxy shape, area 7.00
total 10.14- No
abstractand novirtual: the base method throws, every method is virtual, andoverrideis required.: base(...)is: super(...), and access is set byprivate:andpublic:sections. to_string()isToString():printand${...}call it.
A custom exception and using
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}"); }
}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})`);
}
}lock 1
saved 1
unlock 1
lock 7
unlock 7
failed: no such slot (slot 7)- Any value can be thrown, so
SaveFailedextends nothing, andcatch (SaveFailed e)selects by type. usingandIDisposablebecome a destructor:guardis a value, and~SlotLock()runs whensaveends, by return or by throw.
Feature by feature
Variables and types
| C# | JaiScript | |
|---|---|---|
| inferred, dynamic | var n = 5;, dynamic x = 5; | auto n = 5;, var x = 5; |
| numbers | int (32-bit), long, double | int, 64-bit and always checked; float (or double), 64-bit |
| string, char, bool | the same names | the same names; a string is a value of bytes |
| constant | const, readonly | const int MAX = 10;; no readonly |
| null | null, int? | null, held by a var, a shared_ptr<T> or a function; never by an int or a class value |
| narrowing | int i = (int)3.9; | int i = cast<int>(3.9);; a float never enters an int implicitly |
| collections | new List<int> { 1, 2 }, new Dictionary<string, int> { ["a"] = 1 } | array<int> xs = [1, 2];, map<string, int> m = {"a": 1}; |
| type name | x.GetType().Name | type_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.Keys | m[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, struct | a class is a reference type, a struct a value type | every 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, override | virtual in the base, override in the subclass | every method is virtual; override is required |
| abstract, interface | abstract class Shape, interface IShape | no 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 |
| access | a modifier on each member | public: 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 |
| property | public int Hp { get; set; } | a field, or accessor methods int _get_hp() and void _set_hp(int v) that obj.hp goes through |
| static | Hero.Count | Hero::count; Hero.count and hero.count are load errors that name it |
| ToString | override string ToString() | string to_string(), used by print(x), to_string(x), `${x}` and "text" + x |
| equality | reference equality by default | == compares fields; operator== replaces it; same_object(a, b) is identity |
| operators | public static Vec operator+(Vec a, Vec b) | Vec operator+(Vec o), a method |
as | x as Hero | cast<Hero&>(x), which raises a catchable error when x is not a Hero |
is | x is Hero | x instanceof Hero: a Hero or a class derived from it. type_of(x) == "Hero" is the exact class only |
| using, IDisposable, finally | using (var f = Open()), finally { } | a destructor, ~File(), runs when its scope ends, a throw included; no finally |
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();
}Sir Ada 0 1 hero: Ada (0 hp)
False True Falseclass 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); }Sir Ada 0 1 hero: Ada (0 hp)
false true false
caught a Hero: AdaC# 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 | |
|---|---|---|
| throw | throw new InvalidOperationException("x"); | throw "x";: any value |
| catch | catch (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: " + 5 | a 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 methods | return new strings | s.to_upper(), s.trim(), s.replace(a, b) change s and return nothing; string::to_upper(s) returns a new string |
| parse | int.TryParse(s, out n) | s.to_int(): null when the text is not a number; s.to_int(0) supplies the fallback |
| null operators | x?.Name, x ?? y, x ??= y | the same three |
| switch | every case ends in break | breaks by default; empty stacked labels share a body, and fallthrough; replaces goto case |
| checked | checked { } | always: integer overflow raises, and math::wrap_add wraps on purpose |
| lambda | x => x * 2 | x => x * 2, (int x) => x * 2, or C++'s [](int x) { return x * 2; } |
| delegate, event | Func<int, int>, event Action Clicked | a function slot (any callable, no signature); an array<function> of handlers |
| generic method | T Max<T>(T a, T b) | auto biggest(auto a, auto b) |
| iterator | IEnumerable<int> Evens() { yield return 0; } | coroutine int evens() { yield 0; }, driven by range-for or next() |
| async, await | await Task.Delay(100) | none: a coroutine the host advances each frame; each next() runs to the next yield |
| namespace | namespace Game { }, using Game; | namespace game { } and game::f(); using game::f; brings in one name |
| JSON | JsonSerializer.Serialize(o) | to_json(v), from_json(s) |
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(); }
}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));[0,2,4,6] 2 Ada has 7.2 43 0Watch 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.
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; }inside: 49
49 49
5class 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);inside: 9
9 50
5 1In 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):
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; }5 Trueclass Unit { int hp = 10; }
array<Unit> units = [Unit(), Unit()];
for (auto u : units) { u.hp -= 5; }
Unit nobody = null;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.