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 | |
|---|---|---|
| inferred | var n = 5; | auto n = 5; |
| dynamic | dynamic x = 5; | var x = 5; |
| integer | int (32-bit), long | int, 64-bit; no long |
| floating point | double, float | float, 64-bit; double names the same type |
| string, char, bool | string, char, bool | the same names; a string is a byte sequence and a value |
| constant | const, readonly | const int MAX = 10;; no readonly |
| null | null, int? | null, which a var holds |
| list | new List<int> { 1, 2 } | array<int> xs = [1, 2]; |
| dictionary | new Dictionary<string, int> { ["a"] = 1 } | map<string, int> m = {"a": 1}; |
| type name | x.GetType().Name | type_of(x) |
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());int string 2147483648 3Collections and LINQ
| C# | JaiScript | |
|---|---|---|
| add | list.Add(x) | xs.push(x) |
| count | list.Count | xs.size() |
| search | list.Contains(x), list.IndexOf(x) | xs.contains(x), xs.index_of(x) |
| remove | list.RemoveAt(i), list.RemoveAll(x => x < 0) | xs.remove(i), xs.remove_if(x => x < 0) |
| foreach | foreach (var x in xs) | for (auto x : xs); for (auto& x : xs) changes the elements |
| Where | xs.Where(x => x > 2) | array::filter(xs, x => x > 2) returns a new array; xs.filter changes it |
| OrderBy | xs.OrderBy(u => u.Hp) | xs.sort((a, b) => a.hp < b.hp), in place |
| Select | xs.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, First | xs.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, ForEach | xs.All(p), list.FindIndex(p), list.ForEach(f) | xs.every(p), xs.find_index(p) (-1 when none), xs.for_each(f) |
| Concat, Insert | xs.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 |
| Join | string.Join(", ", xs) | xs.join(", ") or string::join(xs, ", ") |
| ContainsKey | d.ContainsKey(k) | m.contains(k) |
| TryGetValue | d.TryGetValue(k, out v) | m.get(k, fallback) |
| remove a key | d.Remove(k) | m.erase(k) |
| keys | d.Keys | m.keys(), in key order |
| pairs | foreach (var kv in d) and kv.Key | for (auto [k, v] : m), or for (auto kv : m) and kv.first, kv.second |
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));imp elf, ogre 53 0Classes and properties
| C# | JaiScript | |
|---|---|---|
| class | class Hero { public int Hp = 10; } | class Hero { int hp = 10; }: members start public |
| value or reference | a class is a reference type | a class is a value type, like a C# struct; new Hero() makes the shared reference |
| struct | struct Point { } | no struct: every class is already a value type |
| constructor chaining | : this(0, 0), : base(5) | : this(0, 0), : super(5) |
| base call | base.Describe() | super::describe() |
| virtual, override | virtual in the base, override in the subclass | every method is virtual; override is required (writing virtual is an error that says so) |
| abstract, interface | abstract class Shape, interface IShape | neither: a base class with default methods, or a var that takes any object with the method |
| access | public, private, protected, internal on each member | public: and private: section labels; private reaches subclasses, and protected: means the same |
| property | public 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 |
| static | Hero.Count | Hero::count |
| ToString | public 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 |
| operators | public static Vec operator+(Vec a, Vec b) | Vec operator+(Vec o), a method |
| is, as | x is Hero h, x as Hero | type_of(x) == "Hero"; cast<Hero&>(x) raises a catchable error when x is not a Hero |
| using, IDisposable | using (var f = Open()) | a destructor, ~File(), runs when its scope ends |
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);Sir Ada 0 1 Ada (0 hp) true
hero: Ada (0 hp)Errors, strings and the rest
| C# | JaiScript | |
|---|---|---|
| throw | throw new InvalidOperationException("x"); | throw "x";, any value: a string, a number, an instance |
| catch | catch (IOException e) { e.Message; } | catch (e) { e.message; e.stack; e.value; }, untyped (a typed catch is an error that says so) |
| finally | finally { } | none: a destructor runs when its scope ends, a throw included |
| interpolation | $"{name} has {hp:F1}" | `${name} has ${hp:.1f}` |
| format | string.Format("{0}", x) | format("{}", x) |
| string methods | return new strings | to_upper, trim, replace and the other mutators change the string; the string:: versions return new ones |
| parse | int.Parse("42") | "42".to_int(), null when the text is not a number |
| null-conditional | x?.Name, x?.A.B | x?.name; a null x skips the rest of the chain, so x?.a.b is null too |
| null-coalescing | x ?? "none" | the same: x ?? "none", and x ??= v |
| switch | every case ends in break | breaks by default; fallthrough; replaces goto case |
| checked | checked { } | always checked: integer overflow raises; math::wrap_add wraps on purpose |
| lambda | x => x * 2 | the same: x => x * 2, (int x) => x * 2; or [](x) { return x * 2; } |
| delegate, event | Func<int, int>, event Action Clicked | a function parameter or field; 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 that the host advances every frame |
| 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) |
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));[0,2,4,6] 2 Ada has 7.2 43 15Watch 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:
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);0 50
50 50
10Value 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>:
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);10
5 4
1var 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:
var loose = 5;
loose = "now text";
auto locked = 5;
locked = 9.7; // converts: locked stays an int
print(loose, " ", locked);now text 9A value of the wrong type for an auto is a load error when the checker can see it:
auto locked = 5;
locked = "text";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:
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);100.0 212.0
32.0Access 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:
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); }7
caught: Cannot access private member 'secret' of class 'Base'class Base { private int secret = 7; }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.
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));[95,88,72,61] 255 true 1Coroutines 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:
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());
}door 1/3
door 2/3
door 3/3
door doneto_string() speaks for an instance
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:
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());(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:
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);ADA ada ADA ada ADAStatics 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:
class Config { static int volume = 7; }
print(Config::volume);
print(Config.volume);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:
int big = 2147483647;
print(big + 1);
int max = 9223372036854775807;
try { max += 1; } catch (e) { print("caught: ", e); }
print(math::wrap_add(max, 1));2147483648
caught: Integer overflow in '+='
-9223372036854775808A 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.
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]));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.