Types have a per-module namespace. This is pre-populated with int i8 i16 uint u8 u64 etc num float f64 f128 Bool byte char string Types can be added with: struct name: content record name: content enum name: content class name: content name is optional, and can list (parameters) and /attributes Types can be constructed with name(args) parameterized type name^ reference type name[size] array type (args:: args) procedure type (args:: type) function type I think that for pointer/array constructor, the decoration comes first. foo: [5]int is an array of 5 integers, so foo[5] is an int foo: @bar is an owned pointer to bar, so "foo@" is a bar. foo: @[5]int is an owner pointer to 5 integers while foo:[5]^int is an array of 5 borrowed pointers to integers. Having the result type at the end fits better with the function type. foo:(int, int)string then foo(1,2) returns a string. The content of struct and record are a list of: fieldname: type or fieldname/attribute: type 'attribute' can: indicate endianness - bigendian littleendian hostendian 'const' ?? identify a refcount, protected by a given lock?? For enum, content is list of name = value where "= value" is optional Pointers: to assign a pointer, use foo = stuff to update what the pointer points to, use foo^ = stuff to get a reference to store in a pointer.... references are either borrowed or owned. a name defined "type^" is a borrowed reference, a name defined "type@" is owned. borrowed references may be taken of anything, but only remain defined as long as the owner remains defined. owned references can only be taken of ownable objects, and remain indefinitely. There are various ways to own an object: - refcount or lock - ownership of a containing object. - ownership provided by class method non-type names (vars, constants) can be introduced with const prefix: name = value; ... func: name(args::type): statements proc: name(args::result): statements main: statements init: exit: enum name: values Plan: decide on data structure different types can't really be handled by a big switch now, I probably need and object with function pointers. free_value(), vtype_compat(), val_init(), dup_value(), value_cmp(), print_value(), parse_value() parse_value only needed for args - str and num val_init... print_value - only needed for print and code-dump dup_value - needed until pointers can make sense vtype_compat - needed for various things value_cmp - needed until we have object behaviours pre-defined Bool int - sizes char string array I think I need to disassociate the type from the storage. So a 'value' is a parsed constant, but something new is needed for the content of a variable. Also, something new is needed for an intermediate lvalue such as an index to an array or a field in a record. Ths is a type plus a pointer. What about rvalues and the result of a calculation. I guess I store that in a temp location, with a pointer... I need to be clear what "interp_exec()" returns. Conceptually it can be an object of any type, and for procedures it can be a tuple of objects. It needs to identify a type and a value of that type. The "value" might be a reference into a variable, or it might be a copy from a calculation. Eventually the reference will have ownership information const struct Steps: 0/ when does 1/2 produce an integer? Only when explicitly expected. 1/ remove 'tail' from value 2/ define a 'type'. 3/ Add a 'Vtyped' vtype and a 'type' pointer 4/ add an owership enum: borrowed, single 3/ add a 'void *valref' ?? 4/ Convert num bool str label to Vtyped No, this is awkward because propagate_types wants a 'type' and this intermediate format has a enum+pointer. So make it just a pointer... What do I do with Vnolabel - rule flag Vunknown - NULL pointer Vnone - special value A 'type' must be able to: check is it can convert to some other type, reporting if it wants to convert to another type. This requires visibility into other types. print, compare, parse add subtract multiply divide index ---------- I currently have an enum of types that is used to test comparability and for propagation. This needs to change .... I guess I need a struct type* What goes in it? - name - scalar/record/struct/array/pointer/func/enum - other details. Do I need forward declarations? Maybe I can just be lazy and require everything to be declared eventually. That isn't sufficient for: - mutually recursive functions - mutually recursive structures What about unions ??? blend with enum: a tag with fields? struct name: x:int; y:int .Bool.true = a:char; b:char .Bool.false = ...... I like "inheritance" but that doesn't allow the size of the whole to be known in advance. I need a way to talk about which instance is active, even if the value isn't stored. So Pascal-like variant records are good. Maybe a struct could be declared as 'extensible' and other structs could 'extend', and it literally makes it bigger struct a extensible {x1:int} struct b extends a {y1:int} struct c extends a extensible {y2:int} struct d extends c {z1:int} now 'a' has room for x1, (y2 and z1) or y1 Which is used depends on context .. or by assertion on content. So extensions can set values for existing fields struct z extends a {x1=4; string c} Syntax is a bit clumsy. Do I need "extensible"?? Are enums just a fancy way of doing 'const'? I could have const: a=1; b=2; c; d which defines module-wide consts. And separately have enum foo: a, b, c which defines consts foo.a foo.b foo.c But I don't really like foo entering the val namespace. If bar:foo, then bar.a could be true/false depending on value of bar. bar.a! could set it?? bar.a=true? var = .foo.a How do we allocate new objects? new(type) ??? ptrname = new ?? new(ptrname) ptrname := new(type) Maybe ^= assigns a borrowed reference, and @= assigns an owned reference. allocating isn't really a top priority, so I should just focus on non-allocated types. ----- I need a list of steps again: - look up type by name and add syntax for name : type = value and name : type - Add arrays: name : type[size] e.g. int[4] or foo@[3] What is int[5][20] ?? it is an array of int[5], which is backwards. So maybe I want [5][20]int ?? declares an array of that type/size name : [] = [ 1,2,3 ] declares an array of 3 numbers. name[1] extracts an element from the array, name[2] = 4 updates an element name[4:5] creates a slice, which can be stored in a var (borrowed ref) or assigned to This requires: - new type class which has a size and member type - new type access methods: index and size(?) - new syntax - new manifest values: [a,b,c] creates an array of whatever member type. - add syntax for struct name : [[ name : type ]] to define a new struct - add syntax to extra a field from a struct how is this type-checked if I don't know the type yet? - add syntax for pointers a:= new(struct foo) a:struct foo^ = new() ------- Questions: If I declare "struct foo ..." do I use "foo" or "struct foo" to ref the type? I think just "foo". So structs, records, enums, and classes must have distinct names. When do I differentiate between compile-time constants and run-time values? When declaring an array, do I require the size to be constant? In a struct I do ... at time of declaration I calculate the size. For foo:[sqr(a)]int I do that too - and it is at run-time. So during parsing, I need to describe the array with a member-type and executable size. When that is evaluated, a type is created. So we really need an executable which returns a type. But ... we need to know the type when doing type analysis. So while variable size is OK, the compiler needs to know what it is. Maybe the size needs to be a constant, as in a names assiged with "size ::= 4*5". This gives the compiler some chance of comparing types of array - and doing range checking on indexes. We currently call var_init to set the type of a variable during type analysis - which makes sense. But for an array we don't have the final type until run-time. So we need an intermediate type. So (for now) the size of an array is either a NUMBER or an IDENTIFIER which must be a constant var. I need a point where the type is instantiated - where the variable is evaluated and the size is set. I guess this happens when the 'struct var' is evaluated... no, when a Declare binode is evaluated. What happens if I have a:[foo]number = thing I guess the type analysis needs to afirm that thing has the correct type, then a doesn't need to be initialized. If I find a[4] = "hello" and 'a' hasn't been declared .... obviously an error. When/what/how. For field access, I need to know the type of the variable. But I can delay the look up until type analysis. So a[4] is Index(a, 4) - a binode a.foo is Field(a, "foo") - need a new exec type - Fieldname a(args) is Call(a, args) - need a new Binode type - Tuple. So I have to delay 'const' assessment to later too. ---------- Where do type definition go? I don't think they go with statements, they belong separately. I don't want the full separation of a "type" section like Pascal So they probably go at the top level, equivalent to "program" - and before. They start with "struct" or "enum" or "record" etc. So: what about constants? These are currently statements and so affect a scope in time. But for declaring arrays in structs, or initial values of fields, we might want constants. A constant could be within a struct, but only that it too limiting. I need module-wide constants. So I guess: const: name ::= value name ::= value or const { name ::= value ; name ::= value } -------------- I'm in the middle of stage-1 on structures. I need a type to parse the declaration into. It needs to be a linked list of fields, each of which is a type, a name, and an initial value. i.e. a 'struct field'. ----------------- Numbers... I want signed/unsigned/bitset integers (and probably floats). These are different sizes, and I want to move 'type' out of 'value' so I can have arrays of numbers that are *just* the densely packets numbers. So there are two questions here: how will I handle values in oceani, and what are the semantics of numbers in ocean. I think I want bitops to requires bitsets and arith ops to require signed/unsigned. But there is some overlap. e.g. we use bitops to test if a number is a power of two We sometimes use bitops to multiply, but that is probably best avoided. use * to multiply. Converting between the two can be done with simple assignment. So + - * / % require/assume signed or unsigned | & ~ << >> require/assume bitset # accepts either and produces a bitset Other issue is overflow/underflow checking. Do we need another unsigned type - cyclic i32 - signed integer in 32 bits u32 - unsigned integer c32 - unsigned with overflow permitted and ignored b32 - bitset int uint cint bset - whatever size. i32 and u32 detect overflow/underflow and set to NaN - all 1's If I want to allow overloading (such a NaN), I need a type that declare no overloading. s32 and c32? Or annotations. !s32 !u32 So what about values in oceani? I want to separate out the type and not use a union. Where are they used? - return of init, prepare, parse, dup - passed to print, cmp, dup, free, to_int, to_float, to_mpq - field in 'struct variable' - field in 'struct lrval' - result of 'interp' - intermediate left/right in interp - field in array and struct field - field in 'struct val' for manifest constants So: variable gets a 'type' pointer and a union which can be a pointer to the value, or the value itself (depending on size) lrval get a type pointer as well, plus the union interp returns ... ----------------- Struct/array initialisers. I like [a,b,c] rather than {a,b,c} because the latter can look like code. But [] is also array indexing. So an array initializer could look: [ [1] = "hello", [5] = "there" ] and that is confusingly similar to nested initialization [ [1,2] , [3,4] ] Options: 1/ use different outer. {} () <> << >> < is possibly as it is not a prefix operator. But nesting results in <<1,2>,<3,4>> which looks like << instead of < < {} I already don't like () is bad enough with function calls - it is best if it is grouping only. though with function calls it is a list ... << [1]="hello", [2]="there" >>... I don't really like that array[ ] struct[ ] No, too noisy. 2/ use different inner syntax. [ .[1] = "hello", .[5] = "hello" ] What about a newline-based syntax: a: [4]int : [0] = 2 [1] = 3 [3] = 1 Nice, but doesn't actually help. Still need .[] because I want to allow a one-line syntax too. Maybe I just use {} after all. a:[4]int = { [0]=2, [1]=3, [3]=1 } Yes, I guess that is best.