class
General
Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made. Each class instance can have attributes attached to it for maintaining its state. Class instances can also have methods (defined by its class) for modifying its state.
A typical Ysharp class consists of several building blocks:
- Fields (Attributes): Variables that store the object's state (e.g., a Car class might have color and speed).
- Methods: Functions that define the object's actions (e.g., drive() or brake()).
- Constructors: Special methods called when an object is instantiated to initialize its attributes.
Compared with other programming languages, Ysharp's class mechanism adds classes with a minimum of new syntax and semantics. It is a mixture of the class mechanisms found in Java / Javascript. Ysharp uses a prototype based inheritance model. Instead of supporting multiple base classes like classical object-oriented languages, each object has a single prototype, forming a prototype chain. Behavior and properties are resolved dynamically through this chain. a derived class can override any methods of its base class or classes, and a method can call the method of a base class with the same name. Objects can contain arbitrary amounts and kinds of data. As is true for modules, classes partake of the dynamic nature of Ysharp: they are created at runtime, and can be modified further after creation.
Runtime Objects
Types
There are 2 types of data in Ysharp, primitives and runtime objects. Runtime objects also divide into 5 sub-categories.
Function related runtime-objects
- User-defined Functions
- Lambda
- Native Functions
Class related runtime-objects
- Class
- Class-instance
All of the runtime objects implement the Callable interface except Class-instance.
Prototype Chain
Every runtime object has a prototype field inherited from RuntimeObject,
but the prototype chain is only active for Class-instance objects.
For all function-related runtime objects (FunctionObject, LambdaObject,
NativeFunction) the prototype is explicitly set to null their behaviour
is fixed and they do not participate in prototype-based lookup.
Root Prototype
At the top of the chain sits ClassPrototype, a singleton RuntimeObject
with the internal type __RootPrototype__. Its own prototype field is null,
making it the absolute root the chain terminates here.
ClassPrototype ships with two built-in methods available to all instances:
| Method | Arity | Description |
|---|---|---|
getType() | 0 | Returns the runtime type name of the receiver (this) |
getPrototype() | 0 | Returns the prototype object of the receiver (this) |
Both methods resolve this from the current environment at call time.
Lookup
When a field or method is accessed on a Class-instance, the runtime first
checks the instance's own fields map. If not found, it walks up the
prototype chain until the field is resolved or the chain ends at null.
Examples
- prototype chain example
class User {
let f_name : string ;
let l_name : string ;
var fullName = () => this.f_name + " " + this.l_name;
constructor(f_name: string, l_name : string) do
this.f_name = f_name;
this.l_name = l_name;
end
}
sealed class Employee extends User {
let salary : number;
constructor(f_name :string, l_name : string, salary : number) do
super(f_name, l_name);
this.salary = salary;
end
}
const emp = new Employee("yagiz", "erdem", 10000);
println emp.fullName(); // yagiz erdem
println emp.getPrototype(); // <prototype:Employee>
println emp.getPrototype().getPrototype(); // <prototype:User>
println emp.getPrototype().getPrototype().getPrototype(); // <prototype:root>
println emp.getPrototype().getPrototype().getPrototype().getPrototype(); // null
- getType example
class User {
let f_name : string ;
let l_name : string ;
var fullName = () => this.f_name + " " + this.l_name;
constructor(f_name: string, l_name : string) do
this.f_name = f_name;
this.l_name = l_name;
end
}
sealed class Employee extends User {
let salary : number;
constructor(f_name :string, l_name : string, salary : number) do
super(f_name, l_name);
this.salary = salary;
end
}
const emp = new Employee("yagiz", "erdem", 10000);
const user = new User("John", "Ousterhout");
println Employee.getType(); // _Employee_
println emp.getType(); // Employee
println emp.getPrototype().getType(); // __Employee__
println emp.getPrototype().getPrototype().getType(); // __User__
println emp.getPrototype().getPrototype().getPrototype().getType(); // __RootPrototype__
println User.getType(); // _User_
println user.getType(); // User
println user.getPrototype().getType(); // __User__
println user.getPrototype().getPrototype().getType(); // __RootPrototype__
Note
Notice that Class itself type starts and ends with '_' like _User_ , Class-instance type is User and prototype types starts and ends with double '__' like __User__ .
var a = 10;
println a.getType(); // throws error
- Only classes has methods, use explicit type api to get type of other variables like functions and primitives.
Class
Class is a runtime object that inherits from both the RuntimeObject and Callable interfaces. Since it implements Callable,
a class can be invoked directly calling a class object creates and returns a new Class-instance.
Closure
A ClassObject captures the environment at the point of its definition and
stores it as a closure field. This closed-over environment is used when the
class is invoked the constructor and instance methods execute within a scope
that has access to the variables visible at the time the class was declared.
This is the same closure semantics used by FunctionObject and LambdaObject.
This means classes are first-class values in Yshapr they can be defined inside functions or other scopes, and they will correctly capture the surrounding environment just like a function would.
Example
function makeHuman(name) do
class Human {
getName() do
return name;
end
}
return new Human();
end
var h1 = makeHuman("Yagiz");
var h2 = makeHuman("Erdem");
println h1.getName(); // Yagiz
println h2.getName(); // Erdem
Method & Properties
Classes in Ysharp can define both properties (state) and methods (behavior).
Instances inherit methods through the prototype chain and hold their own property values.
class Meal {
let price;
var name;
var id : int;
toString() do
return this.name + " " + this.price + " - " + this.id ;
end
}
const m = new Meal();
m.name = "meat";
m.price = 100;
m.id = 1;
println m.toString();
- var / let / const can be used for property declarations, this has same behaviour with normal variable declarations.
class Test {
var t1;
let t2;
const t3 = "runtime-constant";
}
Advanced Example: Methods, Properties & Closures
function createAccount(ownerName) do
var balance = 0; // private state (closure)
class Account {
var owner = ownerName;
deposit(amount) do
balance = balance + amount;
end
withdraw(amount) do
if amount > balance then do
println "Insufficient funds";
return null;
end
balance = balance - amount;
end
getBalance() do
return balance;
end
// method returning a lambda (closure inside class)
createLogger() do
return () => do
return this.owner + " -> balance: " + balance;
end;
end
toString() do
return this.owner + " (" + balance + ")";
end
}
return new Account();
end
const acc = createAccount("Yagiz");
acc.deposit(500);
acc.withdraw(200);
println acc.getBalance(); // 300
const log = acc.createLogger();
println log(); // Yagiz -> balance: 300
new keyword
The new keyword is used to create a new instance of a class. When invoked, it calls the class's constructor with the provided arguments and returns a fully initialized Class-instance.
Syntax
var instance = new ClassName(arg1, arg2, ...);
How It Works
Calling new on a class triggers the following steps in order:
- A blank
Class-instanceis created. - The
InstancePrototypeis assigned to the instance's prototype chain. - If the class has a superclass, super fields are copied onto the instance (either via explicit
super()or implicit zero-arg call). - Declared instance properties are initialized on the instance.
- The constructor body executes with
thisandsuperin scope. - The fully initialized instance is returned.
Basic Usage
class Point {
var x;
var y;
constructor(x: int, y: int) do
this.x = x;
this.y = y;
end
}
const p = new Point(3, 5);
println p.x; // 3
println p.y; // 5
No-Constructor Class
If a class defines no constructor, it accepts zero arguments and the instance is created with only its declared properties initialized.
class Box {
var width = 10;
var height = 20;
}
const b = new Box();
println b.width; // 10
println b.height; // 20
new with Inheritance
When instantiating a derived class, the parent constructor is also invoked — either explicitly via super() or implicitly with zero arguments.
class Animal {
var name;
constructor(name) do
this.name = name;
end
}
class Dog extends Animal {
var breed;
constructor(name, breed) do
super(name);
this.breed = breed;
end
}
const d = new Dog("Rex", "Labrador");
println d.name; // Rex
println d.breed; // Labrador
new with Sealed Classes
new works normally on sealed classes. Sealing a class only prevents inheritance — it does not affect instantiation.
sealed class Config {
var env = "production";
}
const c = new Config();
println c.env; // production
new with Class Closures
Since classes are first-class values in Ysharp, new can be used on a class returned from a function or stored in a variable.
function makeCounter(start) do
class Counter {
var count = start;
increment() do
this.count = this.count + 1;
end
get() do
return this.count;
end
}
return Counter;
end
const Counter = makeCounter(10);
const c = new Counter();
c.increment();
println c.get(); // 11
Important Notes
newmust be followed by a callableClassObject. Using it on a non-class value is a runtime error.newalways returns aClass-instance— the only runtime object that does not implement theCallableinterface.- Static members defined on the class are not available on the instance returned by
new.
class MathUtil {
static var pi = 3.14;
}
const m = new MathUtil();
println MathUtil.pi; // 3.14
println m.pi; // undefined
static
The static keyword defines a static method or field for a class. Static properties cannot be directly accessed on instances of the class. Instead, they're accessed on the class itself.
Note
Static methods are often utility functions, such as functions to create or clone objects, whereas static properties are useful for caches, fixed-configuration, or any other data you don't need to be replicated across instances.
Static members are defined using the static keyword.
class MathUtil {
static var pi = 3.14;
static add(a, b) do
return a + b;
end
}
println MathUtil.pi; // 3.14
println MathUtil.add(2, 3); // 5
- Static members belong to the class, not the instance.
class MathUtil {
static var pi = 3.14;
static add(a, b) do
return a + b;
end
}
const m = new MathUtil();
println MathUtil.pi; // correct
println m.pi; // undefined
static vs instance example
class Example {
static var s = 10;
var x = 5;
static getStatic() do
return this.s;
end
getInstance() do
return this.x;
end
}
println Example.getStatic(); // 10
const e = new Example();
println e.getInstance(); // 5
Use static members when:
- The data is shared across all instances
- You don’t need per-instance copies
- You want utility-like behavior
this keyword
The this keyword refers to the object on which a method is called.
Its value is determined at call time for methods.
this is not a keyword resolved at parse time, it is a regular Variable
injected into the method's execution environment before the method body runs.
Basic Usage
class User {
var name;
constructor(name) do
this.name = name;
end
getName() do
return this.name;
end
}
const u = new User("Yagiz");
println u.getName(); // Yagiz
Note
Program cannot access instance properties and methods without this keyword, inside of Class-instance. You must write explicit this keyword to access
Static Methods and this
In static methods, this refers to the class itself, since classes are objects in Ysharp.
class App {
static var version = "1.0";
static getVersion() do
return this.version;
end
static getVersion2() do
return App.version;
end
}
println App.getVersion(); // 1.0
println App.getVersion2(); // 1.0
Program can both access static fields and methods via this keyword or class-name itself.
this Inside Lambda Expressions
Lambda expressions do not define their own this.
In Ysharp, this is bound at access time, when the lambda is retrieved from an object.
class Test {
var value = 10;
getFn() do
return () => do
return this.value;
end;
end
}
const t = new Test();
const f = t.getFn();
println f(); // 10
constructor function & super keyword
A class can define at most one constructor method. Defining more than one
is a syntax error at class declaration time.
class Point {
var x;
var y;
constructor(x: int, y: int) do
this.x = x;
this.y = y;
end
}
If no constructor is defined, the class accepts zero arguments and the
instance is created with only its declared properties initialized.
Parameter Type Checking
Constructor parameters support optional type annotations. Each argument is
checked against its declared type before the constructor body executes. A
type mismatch throws a process error:
this Binding
Before the constructor body runs, a fresh environment is created from the
class's closure. The newly created Class-instance is injected into this
environment as this, making it accessible throughout the constructor body.
super Binding
If the class extends another class, the parent ClassObject is injected into
the constructor environment as super. This allows the constructor to call
the parent constructor explicitly via super().
If the child class has a constructor and contains an explicit super() call,
it must be the first statement in the constructor body. Placing it anywhere
else is a process error:
super() must be the first statement in the constructor.
If the child class has a constructor but no explicit super() call, the parent
constructor is called implicitly with zero arguments before the constructor
body runs. If the parent constructor requires arguments, this will fail at
runtime.
If the child class has no constructor at all, the parent constructor is also called implicitly with zero arguments during instantiation.
Example: super Binding
class Animal {
var name;
constructor(name) do
this.name = name;
end
}
class Dog extends Animal {
constructor(name) do
super(name); // must be first
end
}
const d = new Dog("Rex");
println d.name; // Rex
- Missing super() (Implicit Call)
class Animal {
var type;
constructor() do
this.type = "animal";
end
}
class Dog extends Animal {
constructor() do
// no super() call
end
}
const d = new Dog();
println d.type; // animal (implicit super())
- Implicit super() Failure (Parent Requires Arguments)
class Animal {
var name;
constructor(name) do
this.name = name;
end
}
class Dog extends Animal {
constructor() do
// no super(name)
end
}
const d = new Dog(); // runtime error
- super() Not First Statement (Error)
class Animal {
constructor() do
end
}
class Dog extends Animal {
constructor() do
println "before super";
super(); // error
end
}
var d = new Dog();
Instance Initialization Order
When a class is instantiated, the following steps happen in order:
- A blank
Class-instanceis created. - The
InstancePrototypeis assigned to the instance's prototype chain. - If the class has a superclass, super fields are copied onto the instance
(either via explicit
super()or implicit zero-arg call). - Declared instance properties are initialized on the instance.
- The constructor body executes with
thisandsuperin scope. - The fully initialized instance is returned.
Invocation
The constructor is called automatically when a class is instantiated via new.
The ClassObject itself is Callable invoking it triggers the constructor
body with the provided arguments.
var p = new Point(1, 2);
Inheritance
A class can optionally extend another class by referencing a super class name,
stored as superClassName on the ClassObject. When a class inherits from
another, the InstancePrototype of the parent is placed in the prototype chain
of the child's instances so inherited methods are resolved through normal
prototype chain lookup.
Ysharp also supports sealed classes via SealedClassObject. A sealed class
cannot be extended by other classes. Attempting to inherit from a sealed class
is a runtime error.
| Class Type | Extendable | Description |
|---|---|---|
ClassObject | ✓ Yes | Standard user-defined class |
SealedClassObject | ✕ No | Cannot be used as a super class |
Method Resolution via Prototype Chain
Instance methods are not copied onto each instance they live on the
InstancePrototype. When a child class extends a parent, the child's
InstancePrototype prototype is set to the parent's InstancePrototype,
forming a chain:
If a method exists on both the child and the parent, the child's version is found first in the chain effectively overriding the parent's.
Field Inheritance
Instance properties defined on the parent are copied onto the child instance
during construction, either through an explicit super() call or the implicit
zero-argument parent constructor invocation. After copying, the child's own
instance properties are initialized on top if a property name conflicts,
the child's declaration takes precedence.
Note
Fields (properties) are not stored on the prototype.
Unlike methods, fields always belong directly to the instance itself.
Each instance has its own separate copy of fields, and they are initialized during object construction.
Example: Prototype Method Resolution & Field Inheritance
Method Resolution via Prototype Chain
class Animal {
speak() do
return "animal sound";
end
}
class Dog extends Animal {
speak() do
return "bark";
end
}
const d = new Dog();
println d.speak(); // bark (child overrides parent)
Field Inheritance (Copy-on-Construction)
class Animal {
var type = "animal";
}
class Dog extends Animal {
var type = "dog";
var name = "unknown";
}
const d = new Dog();
println d.type; // dog (child overrides)
println d.name; // unknown
sealed Keyword
The sealed keyword is used to prevent a class from being extended.
A sealed class cannot be used as a parent class in inheritance.
Basic Usage
sealed class Animal {
speak() do
return "some sound";
end
}
Invalid Inheritance
sealed class Animal {
}
class Dog extends Animal { // error
}
Error:
Class 'Dog' cannot extend sealed class 'Animal'.
Use Cases
Use sealed when:
- You want to restrict inheritance
- The class represents a final implementation
- You want to protect internal logic from being overridden
- You want to guarantee predictable behavior
Sealed vs Non-Sealed
class Base {
foo() do
return 1;
end
}
sealed class FinalBase {
foo() do
return 2;
end
}
class A extends Base { } // allowed
class B extends FinalBase { } // error
Interaction with Methods
Sealing a class prevents inheritance but does not affect method behavior inside the class.
sealed class Logger {
log(msg) do
println msg;
end
}
const l = new Logger();
l.log("hello"); // works
Important Notes
sealedapplies only to class inheritance.- It does not prevent:
- Creating instances
- Calling methods
- Using properties
- A sealed class behaves like a normal class in every way except it cannot be extended.