From C#​

Much of JaiScript's surface will look familiar from C#: classes with constructors and : this(...) chaining, a required override, static members, properties (spelled as accessor methods), foreach-style loops, try/catch/throw, lambdas, namespaces, and yield for iterators. The semantics underneath are C++'s, and the largest consequence is that a class is a value type: b = a copies the object, the way a C# struct copies. new makes the shared reference that every C# class is. JaiScript's var is the dynamic one, C#'s dynamic; auto is C#'s var. Integers are 64-bit and checked, and a static checker reads the whole program before its first statement runs. This page sets the two languages side by side, then lists the places where a C# habit gets a different answer. Every JaiScript line on it was run on both backends, and the output under each sample is what they printed.

Side by side​

Variables and types​

C#JaiScript
inferredvar n = 5;auto n = 5;
dynamicdynamic x = 5;var x = 5;
integerint (32-bit), longint, 64-bit; no long
floating pointdouble, floatfloat, 64-bit; double names the same type
string, char, boolstring, char, boolthe same names; a string is a byte sequence and a value
constantconst, readonlyconst int MAX = 10;; no readonly
nullnull, int?null, which a var holds
listnew List<int> { 1, 2 }array<int> xs = [1, 2];
dictionarynew Dictionary<string, int> { ["a"] = 1 }map<string, int> m = {"a": 1};
type namex.GetType().Nametype_of(x)
jaiscript
auto n = 5;                     // C#'s var
var anything = 5;               // C#'s dynamic
anything = "text";
int big = 2147483647;
array<int> xs = [1, 2];
map<string, int> m = {"a": 1};
print(type_of(n), " ", type_of(anything), " ", big + 1, " ", xs.size() + m.size());
output
int string 2147483648 3

Collections and LINQ​

C#JaiScript
addlist.Add(x)xs.push(x)
countlist.Countxs.size()
searchlist.Contains(x), list.IndexOf(x)xs.contains(x), xs.index_of(x)
removelist.RemoveAt(i), list.RemoveAll(x => x < 0)xs.remove(i), xs.remove_if(x => x < 0)
foreachforeach (var x in xs)for (auto x : xs); for (auto& x : xs) changes the elements
Wherexs.Where(x => x > 2)array::filter(xs, x => x > 2) returns a new array; xs.filter changes it
OrderByxs.OrderBy(u => u.Hp)xs.sort((a, b) => a.hp < b.hp), in place
Selectxs.Select(f)array::map(xs, f): an array<T> when f declares its return type T; xs.map(f) changes the input and retains its element type
Sum, Any, Firstxs.Sum(), xs.Any(p), xs.First(p)xs.reduce((sum, x) => sum + x, 0), xs.some(p), xs.find(p) (null when none matches)
All, FindIndex, ForEachxs.All(p), list.FindIndex(p), list.ForEach(f)xs.every(p), xs.find_index(p) (-1 when none), xs.for_each(f)
Concat, Insertxs.Concat(ys), list.Insert(i, x)array::concat(xs, ys) returns a new array; xs.concat(ys) appends in place; xs.insert(i, x) inserts in place
Joinstring.Join(", ", xs)xs.join(", ") or string::join(xs, ", ")
ContainsKeyd.ContainsKey(k)m.contains(k)
TryGetValued.TryGetValue(k, out v)m.get(k, fallback)
remove a keyd.Remove(k)m.erase(k)
keysd.Keysm.keys(), in key order
pairsforeach (var kv in d) and kv.Keyfor (auto [k, v] : m), or for (auto kv : m) and kv.first, kv.second
jaiscript
class Unit {
    string name;
    int hp;
    Unit(string n, int h) { name = n; hp = h; }
}
array<Unit> units = [Unit("ogre", 30), Unit("imp", 8), Unit("elf", 15)];
units.sort((Unit a, Unit b) => a.hp < b.hp);
auto healthy = array::filter(units, u => u.hp > 10);
int total = units.reduce((int sum, Unit u) => sum + u.hp, 0);
array<string> names = array::map(healthy, u => u.name);
map<string, int> kills = {"ogre": 2};
print(units[0].name, " ", string::join(names, ", "), " ", total, " ", kills.get("imp", 0));
output
imp elf, ogre 53 0

Classes and properties​

C#JaiScript
classclass Hero { public int Hp = 10; }class Hero { int hp = 10; }: members start public
value or referencea class is a reference typea class is a value type, like a C# struct; new Hero() makes the shared reference
structstruct Point { }no struct: every class is already a value type
constructor chaining: this(0, 0), : base(5): this(0, 0), : super(5)
base callbase.Describe()super::describe()
virtual, overridevirtual in the base, override in the subclassevery method is virtual; override is required (writing virtual is an error that says so)
abstract, interfaceabstract class Shape, interface IShapeneither: a base class with default methods, or a var that takes any object with the method
accesspublic, private, protected, internal on each memberpublic: and private: section labels; private reaches subclasses, and protected: means the same
propertypublic int Hp { get; set; }a field, int hp;, or accessor methods int _get_hp() and void _set_hp(int v) that obj.hp goes through
staticHero.CountHero::count
ToStringpublic override string ToString()string to_string(), used by to_string(x), "text" + x, `${x}` and print(x)
Equals, ==reference 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
is, asx is Hero h, x as Herotype_of(x) == "Hero"; cast<Hero&>(x) raises a catchable error when x is not a Hero
using, IDisposableusing (var f = Open())a destructor, ~File(), runs when its scope ends
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;
print(p.describe(), " ", p.hp, " ", Hero::count, " ", to_string(p), " ", type_of(anyone) == "Paladin");
print("hero: " + p);
output
Sir Ada 0 1 Ada (0 hp) true
hero: Ada (0 hp)

Errors, strings and the rest​

C#JaiScript
throwthrow new InvalidOperationException("x");throw "x";, any value: a string, a number, an instance
catchcatch (IOException e) { e.Message; }catch (e) { e.message; e.stack; e.value; }, untyped (a typed catch is an error that says so)
finallyfinally { }none: a destructor runs when its scope ends, a throw included
interpolation$"{name} has {hp:F1}"`${name} has ${hp:.1f}`
formatstring.Format("{0}", x)format("{}", x)
string methodsreturn new stringsto_upper, trim, replace and the other mutators change the string; the string:: versions return new ones
parseint.Parse("42")"42".to_int(), null when the text is not a number
null-conditionalx?.Name, x?.A.Bx?.name; a null x skips the rest of the chain, so x?.a.b is null too
null-coalescingx ?? "none"the same: x ?? "none", and x ??= v
switchevery case ends in breakbreaks by default; fallthrough; replaces goto case
checkedchecked { }always checked: integer overflow raises; math::wrap_add wraps on purpose
lambdax => x * 2the same: x => x * 2, (int x) => x * 2; or [](x) { return x * 2; }
delegate, eventFunc<int, int>, event Action Clickeda function parameter or field; 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 that the host advances every frame
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)
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& h : clicked) { h(); } }
}
Button b;
int clicks = 0;
b.clicked.push([]() { clicks += 1; });
b.click();
b.click();

namespace game { int spawn_cost(int n) { return n * 5; } }
using game::spawn_cost;
float hp = 7.25;
string name = "  Ada ";
print(to_json(seen), " ", clicks, " ", `${string::trim(name)} has ${hp:.1f}`, " ", "42".to_int() + 1, " ", spawn_cost(3));
output
[0,2,4,6] 2 Ada has 7.2 43 15

Watch out​

The differences below are the ones a C# habit runs into first. Each sample shows what JaiScript does.

A class is a value type​

This is the one to learn first. Assignment, a by-value parameter and a by-value return copy the whole object, as with a C# struct. new gives the shared reference a C# class always is, and a & parameter is C#'s ref:

jaiscript
class Account { int balance = 0; }
Account a = Account();
Account b = a;                  // a copy
b.balance = 50;
print(a.balance, " ", b.balance);

shared_ptr<Account> shared = new Account();   // a shared reference
auto other = shared;
other.balance = 50;
print(shared.balance, " ", other.balance);

void deposit(Account acct) { acct.balance += 10; }        // gets a copy
void deposit_ref(Account& acct) { acct.balance += 10; }   // like ref Account
deposit(a);
deposit_ref(a);
print(a.balance);
output
0 50
50 50
10

Value Semantics & References is the chapter on this, with the cost of each choice.

A list holds values​

An array<Unit> holds the units themselves. A loop variable without & is a copy, so changes to it vanish; auto& names the element, and a method called through an element changes that element. An array of handles behaves like a C# List<Unit>:

jaiscript
class Unit { int hp = 10; void hit() { hp -= 1; } }
array<Unit> units = [Unit(), Unit()];
for (auto u : units) { u.hp -= 5; }        // u is a copy
print(units[0].hp);
for (auto& u : units) { u.hp -= 5; }       // u is the element
units[1].hit();
print(units[0].hp, " ", units[1].hp);
array<shared_ptr<Unit>> handles = [new Unit()];
for (auto h : handles) { h.hp = 1; }       // copies of a handle share the object
print(handles[0].hp);
output
10
5 4
1

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, converting what it can as an int variable would:

jaiscript
var loose = 5;
loose = "now text";
auto locked = 5;
locked = 9.7;                   // converts: locked stays an int
print(loose, " ", locked);
output
now text 9

A value of the wrong type for an auto is a load error when the checker can see it:

jaiscript
auto locked = 5;
locked = "text";
output
jaiscript: static check failed:
<script>:2:8: error: cannot assign 'string' to 'locked' declared 'int' (declared at 1:6)
    locked = "text";
           ^

Properties are accessor methods​

A plain field already reads and writes as obj.x. A computed property is a pair of methods, _get_x() and _set_x(v), and obj.x goes through them; inside the class a bare x is the backing field, if there is one:

jaiscript
class Thermostat {
    float celsius = 20.0;
    float _get_fahrenheit() { return celsius * 9.0 / 5.0 + 32.0; }
    void _set_fahrenheit(float f) { celsius = (f - 32.0) * 5.0 / 9.0; }
}
Thermostat t;
t.fahrenheit = 212.0;
print(t.celsius, " ", t.fahrenheit);
t.celsius = 0.0;
print(t.fahrenheit);
output
100.0 212.0
32.0

Access modifiers are section labels​

private: and public: label every member below them, as in C++; private int x; does not parse. A private member is reachable from the class and from every class derived from it, where C# would need protected:

jaiscript
class Base {
private:
    int secret = 7;
}
class Derived : Base {
    int reveal() { return secret; }
}
print(Derived().reveal());
var b = Base();
try { print(b.secret); } catch (e) { print("caught: ", e); }
output
7
caught: Cannot access private member 'secret' of class 'Base'
jaiscript
class Base { private int secret = 7; }
output
jaiscript: parse failed:
<script>:1:22: error: expected ':' after 'private'
    class Base { private int secret = 7; }
                         ^

LINQ is array methods​

filter, map, reduce, find, some, every, sort and slice cover a LINQ chain. There is no deferred query: each step runs when it is called. The array:: forms (array::filter(xs, f), array::map, array::slice, array::sort) return a new array and leave xs alone, like LINQ; the dot forms (xs.filter(f), map, slice, sort, remove_if) change xs itself, like List<T>.Sort and RemoveAll. A map pairs the same way: map::filter(m, f) returns a new map, m.filter(f) keeps the entries in m.

jaiscript
array<int> scores = [72, 95, 40, 88, 61];
// scores.Where(s => s >= 60).OrderByDescending(s => s).Take(3).Sum()
auto passed = array::filter(scores, s => s >= 60);
passed.sort((int a, int b) => a > b);
int top3 = array::slice(passed, 0, 3).reduce((int sum, int s) => sum + s, 0);
print(to_json(passed), " ", top3, " ", scores.some(s => s < 50), " ", scores.find_index(s => s > 90));
output
[95,88,72,61] 255 true 1

Coroutines instead of async​

There is no async or await. Work that spans frames is a coroutine: each next() runs it to its next yield, and the final return value arrives with the last next(). The host's frame loop does the advancing:

jaiscript
coroutine function fade(string what, int frames) {
    for (int f = 1; f <= frames; f++) { yield `${what} ${f}/${frames}`; }
    return `${what} done`;
}
auto job = fade("door", 3);
while (!job.done()) {
    print(job.next());
}
output
door 1/3
door 2/3
door 3/3
door done

A class's string to_string() is what to_string(x), "text" + x, a template splice and print(x) show, as ToString() is in C#. A class without one prints its class name and fields:

jaiscript
class Point { int x = 1; int y = 2; string to_string() { return "(" + x + ", " + y + ")"; } }
class Plain { int x = 1; }
Point p;
print(p);
print(to_string(p), " ", `${p}`, " ", "at " + p);
print(Plain());
output
(1, 2)
(1, 2) (1, 2) at (1, 2)
Plain{x: 1}

Strings change in place​

A string is a value, so a copy never sees a later change, but the mutating methods change the string they are called on and return it. The string:: functions return a new string instead:

jaiscript
string name = "ada";
string copy = name;
string upper = name.to_upper();       // changes name and returns it
print(name, " ", copy, " ", upper, " ", string::to_lower(upper), " ", upper);
output
ADA ada ADA ada ADA

Statics use ::​

Config::volume reaches a static. Config.volume is an error that names the right spelling: the checker reports it at load, and in a [[warn_mode]] region it raises the same text when it runs:

jaiscript
class Config { static int volume = 7; }
print(Config::volume);
print(Config.volume);
output
jaiscript: static check failed:
main.jai:3:14: error: 'Config' is a class; use Config::volume
    print(Config.volume);
                 ^

Integers are 64-bit and checked​

There is no wrap at 231, and overflow at 263 raises instead of wrapping, as if every block were checked. math::wrap_add, wrap_sub and wrap_mul are the unchecked arithmetic:

jaiscript
int big = 2147483647;
print(big + 1);
int max = 9223372036854775807;
try { max += 1; } catch (e) { print("caught: ", e); }
print(math::wrap_add(max, 1));
output
2147483648
caught: Integer overflow in '+='
-9223372036854775808

A complete program​

A small shop in the shape a C# programmer would write it: a class with a read-only property and an event, a shared instance, a lambda handler, an iterator feeding customers, and a filter over the stock.

jaiscript
class Item {
    string name;
    int price;
    Item(string n, int p) { name = n; price = p; }
    string to_string() { return `${name} (${price}g)`; }
}
class Shop {
private:
    int gold_value = 100;
public:
    array<Item> stock = [];
    array<function> on_sale = [];
    int _get_gold() { return gold_value; }          // a read-only property
    bool sell(string name) {
        for (int i = 0; i < stock.size(); i++) {
            if (stock[i].name == name) {
                Item sold = stock[i];
                stock.remove(i);
                gold_value += sold.price;
                for (auto& h : on_sale) { h(sold); }
                return true;
            }
        }
        return false;
    }
}

shared_ptr<Shop> shop = new Shop();                 // one shared shop, like a C# reference
shop.stock = [Item("sword", 40), Item("potion", 5), Item("shield", 30)];
array<string> receipts = [];
shop.on_sale.push([](Item it) { receipts.push("sold " + it); });

coroutine customers() {
    yield "potion";
    yield "axe";
    yield "sword";
}
for (auto want : customers()) {
    if (!shop.sell(want)) { receipts.push("no " + want); }
}
auto cheap = array::filter(shop.stock, [](Item it) { return it.price < 50; });
print(receipts.join("; "));
print("gold: ", shop.gold, ", left: ", cheap.size(), " ", to_string(cheap[0]));
output
sold potion (5g); no axe; sold sword (40g)
gold: 145, left: 1 shield (30g)

Where to next​

Value Semantics & References is the chapter to read before anything else. The Type Ladder explains var, auto and typed slots, Classes constructors, accessors, access control and hot reload, and Coroutines the handles that replace async. The ten-minute tour builds a small game with all of it.