Z# (Zee-sharp)

A new .NET language

Types

There are built-in types that define the basic data storage widths and semantics.

Several flavors of custom types can be defined:

There is a basic syntax that all types declarations follow:

TypeName: BaseType
    #meta = value   // data type constraints (#)
    Enum = value    // enum named values
    Field: Type     // struct fields

TypeName: The name of the type.

BaseType: (optional) the name of the type this new type is based on (derives from).

Then each type has its own way of specifying its implementation:

#meta refers to a compile-time meta property that can be used in a DataType rule to restrict the value range of the defined type. See ‘Custom Data Types’ further down.

Enum is used to define a named value type. It is optional to specify an explicit value. See also Enums.

Field sets the defined type up as a data structure containing one or more fields. See also Structures.

All type names start with a Capital letter

TBD: instead of making a distinction between structs and enums and custom types, have one type that can be a combination of any of these aspects - except custom data types are always a singular value, but can be combined with enum-values -or- can #rules also be applied to structs and enums?

MyType
    Option1, Option2
    fld1: U8
    fld2: Str
    #fld1 > 42  // -or-
    #fld1.value > 42

How to declare different Enum types?

MyType
    Option1: U8, Option2: U8
    Option10: Str, Option11: Str
    fld1: U8
    fld2: Str
    #fld1 > 42

How to apply an enum as data type for a field in the same type?

MyType
    Option1, Option2
    fld1: ??

Built-in Types

The built-in data type form the basic building blocks for creating structures. There are built-in types for integers, floating point, string and boolean.

Other .NET types are not a native part of Z# and can be used as an external library type.

.NET has other types in System.Numerics that may be of interest to include in the future as native Z# types.

Integers

The type names have been shortened to an absolute minimum. First:

TBD: Or should we use ‘S’ for signed integers?

followed by the width in the number of bits.

U8 U16 U32 U64
I8 I16 I32 I64

These map to respective .NET types:

TBD: Do we want an autoscaling Integer type? (.NET System.Numerics.BigInteger)

TODO: Research where signed and unsigned integers should be used.

Unsigned integers can never overflow, they can only wrap around. Do you still want an overflow exception in a checked context?

Are sizes unsigned? Why is an index signed? Are there problems for signed integers?

What is we introduced unsigned integers that are guarenteed to convert to their signed counterpart? For instance a U31 that can be (implicitly) converted to a I32. Pro: be explicit about what parameters (variables) accept negative values and which don’t.

Floating Point

The floating point data types are:

F16, F32, F64, F96

These map to respective .NET types: Half, Single, Double and Decimal.

TBD: Rational Numbers

Now that dotnet supports number interfaces we could introduce a rational number type that stores decimals not as a floating point representation but as an integer (numerator) with a scaling factor (denominator).

This would fix 0.1 + 0.2 - 0.3 problem: floating point math says it’s 5.551115123125783E-17 and that’s not zero.

R16, R32, R64, R128

For an R32 both the numerator and the denominator would be 32 bits.

https://github.com/tompazourek/Rationals/tree/master

Strings

A ‘string’ of characters of text (UTF-16).

Mut<Str>    // mutable string: StringBuilder
Str         // immutable string: String

This type maps to the .NET string or StringBuilder depending on if Mut<Str> is used.


I am thinking of making each encoding available in a dedicated type which can be converted to one another.

StrASCII    // 8-bit
StrUtf7
StrUtf8     // most common
StrUtf16    // explicit .NET string
StrWin1251
StrWin1252
StrIso8859

For these specialized string, no character type is available (other than U8/byte), they would basically encapsulate byte buffers and do not derive from Str.

The encoding and decoding can be viewed as a conversion between different string types.

s := "Hello World"   // Str (UTF16)
s8 := s.StrUtf8()    // convert

loop c in s
    // use c: C16

loop c in s8
    // use c: U8

The conversion is done using the .NET Text Encoding types from the BCL.

Secure String

ss: SecStr = "Secret String"

Uses .NET System.Security.SecureString.

Str-Of-T

TBD

StrOf<T> to allow a described string. T describes the structure of the content of the string. StrOf<EmailAddress> where EmailAddress contains validation (regex?). Related to custom data types.

TBD: this is basically a value object. So other types like I32Of<T> are also possible - where T contains a type that validates and describes the value object.


Character

A single character used in UTF-16 strings.

C16

This type maps to the .NET char.


Boolean

The boolean data type is defined as:

Bool

It can only have one of two values: true or false.

This type maps to the .NET bool.


Bits

The Bit type is parameterized to specify the number of bits the value contains.

Here the example declares a Bit type that contains 4 bits (nibble):

Bit<4>

When Bits are stored, the closest fitting data type is used. So a Bit<6> would take up a single byte U8, while a Bit<12> would take up two bytes U16. Bits are always interpreted as unsigned and stored in the lower bits of the storage type. The upper unused bits are reset to zero.

This type maps the .NET System.Collections.BitArray or System.Collections.Specialized.BitVector32. Possibly some of System.Numerics.BitOperations will be used for some of the operations.


Date and Time

DateTime, Date(only) and Time(only).


Function Type

The function type Fn<T> is used when the type of a function is used in code but no Function Interface is available.

// returns a void function (fn: ())
makeFn(p: U8): Fn
    ...

f = makeFn(42)
f()         // call returned function

Note that the type of a function has a specific syntax:

<generic-types>(parameter-types): return-type

Templates? In order to incorporate Templates two flavors of FunctionTypes must exist: a compile-time and a run-time version. The compile-time version contains information about template-parameters (type parameters and normal parameters). The run-time version only contains information about generic type-parameters and normal parameters.

// returns a function that takes one param (U8) and returns a Str
makeFn(p: U8): Fn<(U8): Str>
// returns a function that takes two params (Str and U8) and returns a Bool
makeFn(p: U8): Fn<(Str, U8): Bool>
// returns a function that takes one param (U8) and has no return
makeFn(p: U8): Fn<(U8)>

This type will map to the .NET Func<T> and Action<T> types depending on the return type.


Void

In light of .NET interop we need to rethink this.

A special type to allow to be explicit when there is no (function return) Type. Acts as the functional Unit in that it has only one value: itself and therefor holds no information.

VoidFn: ()    // no return type: Void
    ...

v = VoidFn()    // legal: v => Void
// you can't do anything with v, though.

NoParamsFn: (Void): U8    // Error: Void not allowed here
    ...

Introducing the Void type removes the necessity to distinguish between functions with or without a return value. See the ‘Void’ topic in Functions for more info.


Literal Numerical Values

Literal values are commonly use in programs and by default the compiler will assign the smallest data type to fit the literal numerical value. There are times when you want to override that, however.

a := 42          // a: U8
b := U16(42)     // b: U16
c: U32 = 42     // c: U32

We are simply calling a dedicated constructor function with the literal value.

Coercing literals to bigger (compatible) types.

// some sort of postfix?
a := 42L // U64?

Custom Data Types

(Constrained Types? / Type Constraints)

There is an easy way to create data types to differentiate data at a type level. By using different types the purpose of the data become even more clear.

The idea here that you can define the information your application deals with without any context. These information definitions can be combined to form larger concepts (e.g. Person).

Only simple data types can used as base type. ?Is this still needed? Why?

Age: U8 _           // no rules
PersonName: Str
    #length < 100   // rules
    #length > 0
Age: U8 _
PersonName: Str _

a: Age = 42
name: PersonName = "John"

The use of _ is optional in most cases. It signifies that nothing follows the declaration.

You do have to use the explicit type on the variable declaration, using defaults will yield standard types (U8 and Str).

Here’s an example of the use of data types.

MetricLength: U16 _
ImperialLength: U16 _

ml: MetricLength = 200      // 200 meter
il: ImperialLength = 200    // 200 yards

loa := ml + il       // error: cannot add different data types

// overloading the + operator would fix this error
Add: (left: MetricLength, right: ImperialLength): MetricLength
    ...
// adding a convertor method
MetricLength: (l: ImperialLength): MetricLength
    ...
// allows this code
loa := ml + il.MetricLength()

Operators of the underlying type can NOT be used. Data Types are always more specific and restrictive than the underlying type. New operator implementations have to be created using the dedicated function names mapped to each operator. It is a compile error if such a function is not found for the operator in use.

It is strange that a custom data type ‘derives’ from a base type but is not polymorphic to that base type!? If we make this polymorphic as one would expect, all operators/functions of the underlying base type can be used. If a custom data type needs to be more restrictive it can overload the operator based on its own type.

I think in the majority (if not all) of cases these functions can be generated by the compiler when rules are defined. But overriding with custom functions must always be possible.

There may be some operators of the underlying type that can safely be used (compare?). The problem mainly exists for operators that modify the value, not for operators that report on the value. There are also other functions that take the underlying type as a (self) parameter that can be reused (will compile). Here we cross into cross cutting concerns: Printable, Orderable etc…

Age: U8 _
a: Age = 42
// use conversion to get to underlying types
u := a.U8()      // u: U8

What result type promotion will there be for arithmetic operators on custom data types? Will simply the return type of the operator-function determine the result type?

Based on the rules of a Custom Data Type, the value range of the result can be determined at compile time and an appropriate (smallest) return type can be chosen.

Note that numerical literals could also be thought of restricted custom data types (in a way). A constant value of 2 only has a small impact on the number of extra bytes needed for the result after an arithmetic operation.

The way data types differ from using aliases is in the use of type-bound functions.

Alias = U16
DataType: U16 _

baseFn(self: U16, p: U8)
    ...
aliasFn(self: Alias, p: U8)
    ...
typeFn(self: DataType, p: U8)
    ...

a: U16 = 0x4242
a.baseFn(42)        // U16 = Alias
a.aliasFn(42)       // Alias = U16
a.typeFn(42)        // error! U16 != DataType

d: DataType = 0x4242
d.baseFn(42)        // error! DataType more specific than U16
d.aliasFn(42)       // error! DataType is not Alias (or lower to base??)
d.typeFn(42)        // ok

Custom Data types cannot have any fields. A struct can be composed of fields using (only) data types.

Age: U8
    #value = 0
    #value = [2..101]   // Range syntax
Mode: Str       // this would be a Str-Enum...
    #value = "A"
    #value = "B"
    #value = "C"
Length: I16
    #value > 0

overload/override value setter (assignment operator?) to do custom validation? Can this code be generated at compile-time by the compiler?

Physical units problem. Can we use Custom Data Types to define a physical units library? Example: acceleration = speed * time-squared (composition?). Also 1000m = 1km (prefixes on dimensions) Units (m) with value(quantity)-scope (minutes), scaled units (km). Perhaps we call these Semantic Types and will have a value, a scale and a unit…?

// these mappings could also be enums?
ScaleKilo:
    #1000 => kilo
    #100 => hecto
    #10 => deca
    #0 => _
    #0.1 => deci
    #0.001 => milli
    #0.000001 => micro
UnitMeter:
    #m => meter
MeterValue: SemanticType<ScaleKilo, UnitMeter>

scale: (val: MeterValue, scale: ScaleKilo): MeterValue
    ...

// ??

Literal Values

Make a literal value have a custom data type.

MyType: U16 _

a := MyType(42)  // passes all rules
SmallType: U8
    #value = [0..4]     // 0,1,2,3

a := SmallType(42)       // Error: does not pass rules
MidType: U8 _

// does not fit into base type
a := MidType(275)        // Error: does not pass rules

Again, we’re simply calling dedicated construct functions.


Custom Data Constraints

The rules that can be defined after the type declaration narrow the value range based on its base-type - in a declarative manner.

By default the specified rules are ANDed together in that all the specified rules must pass before a value assignment will succeed.

How to create ORed rules?

CustomDataType: U8
    #value > 10 or #value = 0

Support ranges as constraint values.

CustomDataType: U8
    #value = [1..43]    // 1 - 42

String-rules may need extra features to match different aspects of the value. The goal is NOT to create a Regular Expression engine, but just cover the basic stuff. For complex validation a custom validation function (override) has to be defined.

CustomDataType: Str
    #value += "Abc"     // starts with
    #value =+ "Abc"     // ends with with
    #value +=+ "Abc"    // contains
    #value -= "Abc"     // does not start with
    #value -=- "Abc"    // does not contain
    #value <> "Abc"     // not equal to

TBD: do we want a custom error message text to go with the rule?

CustomDataType: Str
    #length <= 100, "Too long"

Custom Data Type Conversion

Any conversion to or from a custom data type must be hand written. The compiler cannot know how/what to convert when/where.

// custom types
Hour: U8 _
Minute: U16 _
Second: U16 _

// conversion functions
Minutes: (self: Hour): Minute => self * 60
Seconds: (self: Minute): Second => self * 60
Seconds: (self: Hour): Second => self * 3600

h: Hour = 2
m := h.Minutes()     // m = 120
s := m.Seconds()     // s = 7200
s := h.Seconds()     // s = 7200

Built-in Wrapper Types

Several wrapper types are used to add meaning to other types.

Type Meaning
Err<T> T or Error
Opt<T> T or Nothing
Ptr<T> Pointer to T
Mut<T> T is Mutable
Atom<T> Atomic access
Async<T> Asynchronous function (return type)
Mem<T> Heap allocated (TBD)
In<T> In parameter (TBD)
Out<T> Out parameter (TBD) Use Mut<T> as out parameter?
Ref<T> Reference (in/out) parameter (TBD)

Note that the compiler may generate different code depending on where these types are applied.


Types Operators

Some built-in (decorator) types are use so often, it makes sense to provide a shorter version in the form of an operator.

Type Operator
Err<T> !
Opt<T> ?
Ptr<T> *
Ref<T> &
Mut<T> ^

These operators are always used directly after the type (post-fix)

// optionaL parameter and optional or error return
fn: (p: U8?): U8!?

The precedence can be set according to the order (left to right) the operators appear in the code. Closest to the type is inner: U8?* => Ptr<Opt<U8>>.

o: U8?^     // Mut<Opt<U8>>
p: U8!*     // Ptr<Err<U8>>

The optional and error types can be thought of as constrained variant types.

Opt<T>: T or Nothing _
Err<T>: T or Error _

Nothing is an no-value indication for the compiler and is never available to the program.

Opt is discussed in more detail here.

Err is discussed in more detail here.

Ptr is discussed in more detail here.

Ref is discussed in more detail here.

Can we supply a hash code on Immutable types automatically?

o: U8?      // optional U8
e: U8!      // U8 or error
x: U8!?     // optional U8 or Error
p: U8*?     // optional pointer to an U8
i: U8^*?    // pointer to an optional immutable U8

Err<T> is typically (only) used on function return values.


Immutable Types

TBD: now that the default is immutable, this should perhaps be reconsidered.

Any type can be made immutable wrapping it in a Imm<T> type.

// any old struct
MyStruct
    fld1: Mut<U8>
    fld2: Mut<Str>

// an immutable version of MyStruct
ImmStruct: Imm<MyStruct>
// ImmStruct
//   fld1: U8
//   fld2: Str

The compiler will generate a new Type (struct) based on MyStruct making all fields immutable. All references to immutable types are tracked as immutable.


Immutable References

Variables are of immutable types by default.

a: Mut<U8>  // mutable
a = 42      // ok, a = 42

// immutable has to be initialized on declaration
b: U8 = 42     // init with 42
b = 101        // error! type is immutable

Immutable reference to mutable object.

a := 42  // immutable U8

// explicit conversion to mutable
b: Mut<U8> = a
b = 101             // ok, type is mutable

Immutability in these cases it tracked by the references.

TBD: ‘with’ in some other languages. Mutations on an immutable type results in a new instance.

s: Struct
    ...         //   init struct

// s2 is copy of s with changed field
// what syntax?
s2 := s => { fld1 = 42 }     // object construction
s2 := s.Mut({ fld1 = 42 })   // explicit function call1
s2 := s.Clone({ fld1 = 42 }) // explicit function call2
s2 := s.With({ fld1 = 42 })  // explicit function call3
s2 := s + { fld1 = 42 }      // special operator1
s2 := s & { fld1 = 42 }      // special operator2
s2 := s <= { fld1 = 42 }     // special operator3 (mapping)
s2 := s <- { fld1 = 42 }     // special operator4
s2 := s <+ { fld1 = 42 }     // special operator5

TBD: type validation after construction? This is a general issue…

In all these cases the period of time that the new instance is mutable (when a new instance is created and the old and the new values are copied in) is managed by the compiler and shielded from the developer/program.

A Custom Constructor Function for these immutable types has to take a partial type parameter. By default this constructor is generated by the compiler.

What about using tuples? What if we don’t want an explicit optional type variance to exist?

MyStruct
    fld1: U8
    fld2: Str

// normal constructor function
MyStruct: (p1: U8, p2: Str): MyStruct
    ...

// all fields optional
MyStructOpt : Opt<MyStruct>

MyStruct: (MyStruct self, MyStructOpt change): MyStruct
    ...

If no custom constructor is defined for these immutable object manipulations, the compiler will generate one that performs the merging of self and the changes into a new instance.


Type Comparison / Checking

There are several ways to compare types.

t1: Struct1
t2: Struct2

// exact match
if t1#type = Struct2
if t1.#type = t2.#type

// match expression
match t1
    s1: Struct1 -> ...
    s2: Struct2 -> ...

// implements / castable
if t1.#type is Struct2
if t1.#type is t2.#type

Type Alias

Provides a new name for an existing type. Similar to declaring a new type but without any additions.

// a real new type (struct)
MyType: OtherType<Complex<U8>, Str> _   // _ to indicate no fields

// another name for the same type (alias)
MyType = OtherType<Complex<U8>, Str>

During compilation all references to type aliases are replaced with their original types. Compiler-issues are reported using the original type alias name.


Anonymous Types

Only Structure Types can be implemented as a nameless type.

See also Anonymous Structures.

.NET C# has three different types of anonymous structures: anonymous types (class), value tuples and tuples (class). https://docs.microsoft.com/en-us/dotnet/standard/base-types/choosing-between-anonymous-and-tuple#key-differences


Type Constructors

Should this go in ‘functions’?

Any function can be a factory (function). Type constructors are checked specifically by the compiler to make sure they return new instances of a type.

A type constructor is a function with the same name as the type it creates and returns.

Does the constructor function name has to be the exact same as the original Type definition or do we allow the identifier naming rules to apply? Make the constructor function name the exact same as the return type used in the constructor function.

MyType: (): MyType  // valid constructor function
Mytype: (): MyType  // not a constructor function
// (function name not exact match with return type)

The number and types of parameters a constructor function takes have no restrictions.

MyType
    ...

MyType: (p: U8): MyType
    ...

t := MyType(42)
// t is an instance of MyType initialized with 42

Using templates

MyType<T>
    ...

MyType: <T>(p: T): MyType<T>
    ...

t := MyType(42)
// t is an instance of MyType initialized with 42 (T=U8)

A name-clash can occur when you introduce a new Type with a name that (falsely) matches an existing function as its constructor function. Unfortunately that would mean you would have to alias the existing function to a different name at the locations where it is used and that also reference the new Type. See Import for more on aliasing.

When construction of the type is not trivial and Errors may occur the type constructor function has to have a return type of Err<T>.

MyType
    ...
MyType: (p: U8): MyType!

t := try MyType(42)

Struct will probably be .NET record structs and cannot be returned by ref.

Passing parameters to the base type.

BaseType
    ...
BaseType: (p: Str): BaseType
    ...

MyType: BaseType
    ...
MyType: (p: Str): MyType
    BaseType(p)     // return value?
    ...

TBD: do not allow this. There is always but one constructor function that create an instance of a type, even if that type has a base type.


Type Constructor Overloading

A Type Constructor function can be overloaded - normal functions can only be overloaded based on the self parameter.

At Compile-Time the correct overload will be determined and a compilation error will be generated when no suitable overload was found.

MyType
    ...

MyType: (p1: U8): MyType
    ...
MyType: (p1: U8, p2: Str): MyType
    ...
MyType: (p1: U8, p2: Str, p3: U16): MyType
    ...

// uses overload with 3 parameters
t := MyType(42, "42", 0x4242)

Constrained Variant

Also known as Discriminated Unions (sort of).

OneOrTheOther: Struct1 or Struct2
OneOfThese: Struct1 or Struct2 or Struct3 or Struct4

s: OneOfThese
    ...

v := match s
    s1: Struct1 -> s1.fld1
    s2: Struct2 -> s2.val2
    s3: Struct3 -> s3.bla
    s4: Struct4 -> s4.myfld

The type-id is stored with the instance. Access with #varId or something?

A Ptr should still point to the payload of the variant so perhaps store the var-type in front of the main payload.

A constrained variant instance cannot change type during its lifetime.

TBD: have other means of determining the type of the contents?

OneOfThese: Struct1 or Struct2 or Struct3 or Struct4

s: OneOfThese

// a property that matches T
if s.type = Struct1
if s.#type = Struct1
    ...

// can also use a match expression

This would be basically how the type would be stored…

TBD

Discriminated Unions are similar except they name the type of the union/variant.


OneOrTheOther :|    // <= special syntax is required
    s1: Struct1
    s2: Struct2

s : OneOrTheOther =
    s1 = Struct1
        ...

v := match s
    .s1 -> s1.fld1
    .s2 -> s2.val1

Type Manipulation

Related variations can be created easily from existing types.

// type is immutable by default
MyStruct
    fld1: U8
    fld2: U16

// make type optional
MyOptionalStruct: Opt<MyStruct>
MyOptionalStruct: MyStruct?     // language supported
// fld1: U8?
// fld2: U16?

// make type writable
MyReadOnlyStruct: Mut<MyStruct>
MyReadOnlyStruct: MyStruct^
// fld1: U8^
// fld2: U16^

Using Mut<T> and Opt<T> on the base type applies to all fields.

=> Maybe use different types that indicate a full type transformation? Immutable<T> and Optional<T> (as well as Required<T>)? Or use a # to indicate the compiler trick? MyOptType: #Opt<MyType>

How can this mechanism be extended by 3rd party code?

Perhaps allow manipulation like in TypeScript ‘for each key’…?

TBD Inverse of Opt<T> (required)? Inverse of Mut<T> (Immutable Imm<T>)?

MyStruct
    fld1: U8?
    fld2: U16?

// ??
NonOptStruct: Required<MyStruct>

Make an instance read-only:

s: MyStruct
// using a conversion to make immutable
r := s.Imm()
r.fld1 = 101        // error! field is read-only

//-or-  cast will convert
r: MyReadOnlyStruct = s

Make an instance optional:

s: MyStruct
// using a conversion to make optional
o := s.Opt()
o.fld1 = _        // field is 'nulled'

//-or-  cast will convert
o: MyOptionalStruct = s

There can be special syntax for manipulating instances of (for instance) optional types and their fields?

MyStruct
    fld1: U8
    fld2: Str

MyStructOpt : Opt<MyStruct>

s : MyStruct =
    fld1 = 42
    fld2 = "101"

o : MyStructOpt =
    fld2 = "42"     // partial init

x := s + o       // what if these objects have a + operator defined?
x := { s + o }   // to differentiate from (overloaded) plus operator.
// x => MyStruct
// x.fld1 = 42
// x.fld2 = "42"

p : MyStructOpt =
    fld1 = 101  // partial init

y := o + p
y := { o + p }   // object logic
// y => MyStructOpt
// y.fld1 = 101
// y.fld2 = "42"

Or is the an object mapping and should we use the < operator to merge object instances?


TBD

Ideas…

The |, & and ^ operators act on the memory of a type (sort of).

and and or operate on the logical type.

In no case can a type ever be empty.

What about marker interfaces?


Unions

How are shared fields (locations) initialized when two structs have different default values? Or simply init to zero-always.

What happens if -part of- a field is accessed through another -incompatible- type? For instance: Str|U8 write Str="42" and read through U8. (also a problem in C). COM-interop (.NET) disallows this. Ultimately we must comply with .NET.

Implement this as discriminated union? Difference with constrained variant?

MyUnion1: Struct1 | Struct2
MyUnion2: Struct1 | Struct2 | Struct3

MyUnion             // all fields share the same memory
    fld1: U8 |
    fld2: U16 |
    fld3: Str |     // trailing | ok

// this syntax might be easier with parsing:
MyUnion
    | fld1: U8
    | fld2: U16
    | fld3: Str

Because there is no union keyword, anonymous or inline unions are not possible.

A union can be used with any type or struct.

Union1
    fld1: U8 |
    fld2: U16
Union2
    prop1: Str |
    prop2: Bool

// one big union
Union3
    un1: Union1 |
    un2: Union2

// struct with 2 unions
Struct1
    un1: Union1
    un2: Union2

Type Commonality

Common fields in all types.

MyStruct: Struct1 & Struct2

A compiler error is generated if no fields are common - the type defined is empty.


Type difference

Think of this as an inverse union.

Difference: Struct1 ^ Struct2

Can the |, & and ^ be combined in one declaration? What is the precedence of these operators? => No precedence, use brackets.

MyStruct: (Struct1 & Struct2) | (Struct3 ^ Struct4)

Subtracting from Types

Create a new type based on an existing type minus some fields.

// syntax?
MyStruct: BaseStruct - { fld1: Str, fld2: I32 }

Adding to types is a simple matter of:

MyStruct : BaseStruct
    addFld1: Str
    addFld2: I32

This suggests inheritance and .NET does not allow inheritance on structs.

But more consistent would be something like:

MyStruct: BaseStruct + { fld1: Str, fld2: I32 }

Or can we write subtracting like this?

MyStruct: BaseStruct - 
    fld1: Str
    fld2: I32

Which is kind of confusing…


Multiple Inheritance

Not real inheritance!

Type addition.

MyStruct: Struct1 and Struct2

Laid out in memory in order of definition.

How to move the ‘self’ pointer? => All info is available at compile-time.

MyStruct: Struct1 and Struct2
structFn(self: Struct2, U8)
    ...

s: MyStruct
    ...

s.structFn(42)      // the 'reference' to the structFn has to go past Struct1
// Also goes for Ptr's to sub-parts

// explicit offset? (verbose!)
structFn(s#offset(Struct2), 42)

Dynamic Type

Should dynamic types be taken into account? How would the syntax look and what semantics are attached?

d: Dyn              // dynamic type
d.prop1 = 42        // creates a new field (fixed type)

MyFunction: (self: Dyn, p1: U8): Bool
    // does field exist
    if self?#prop1
        return self.prop1 = 42

    // does function exist
    if self?#getMagicValue
        return self.getMagicValue() = 42
    return false

if d.MyFunc(42)
    ...

use of # in prop exists test is NOT at compile time - we should not use it.

What functions are available to which Dyn instances?

Calling non-existent functions should raise an (runtime) Error. Accessing non-existing fields returns 0? -or- always requires a check in the same scope (like optional)?

Can fields be removed?

d: Dyn
d.prop1 = 42    // now you see it
d.prop1 = _     // now you don't

Can functions be assigned to instances?

MyFunction: (self: Dyn, p1: U8): Bool
    ...

d: Dyn
// depend on dot-syntax here!
d.MyFunc = MyFunction

if d.MyFunc(42)
    ...

d.MyFunc = _    // function is removed

Parsing free data structures (json) into a dynamic type.

d: Dyn <= "{ 'data':'hello world' }"
if d?#data
    // use d.data

The Any Type

Represents the .NET System.Object type for reference cases. (External) Type definitions still use System.Object as base type.

funcAny(): Any
    ...

a := funcAny()   // a => Any
v := match a
    n: U8 => n
    _ => Error("Unsupported")

// v => U8!

TODO: check C++ std::any


TBD

Use meta programming to create types.

// special type assignment
MyType =: #Type("MyType", ...)

Meta programming needs more work.


Other types that need close integration with the compiler?