Skip to content

[SPEC]: Vector Semantics? - #106

Open
Sir-NoChill wants to merge 1 commit into
masterfrom
sir-nochill/vector-vs-array
Open

[SPEC]: Vector Semantics?#106
Sir-NoChill wants to merge 1 commit into
masterfrom
sir-nochill/vector-vs-array

Conversation

@Sir-NoChill

Copy link
Copy Markdown
Collaborator

I am not entirely sure I understand the current reading of vectors and would like to clarify it. I have a type lattice proposed that looks as follows:

type_lattice_proposed

Does this align with what you are going for? Are arrays and vectors able to be used interchangably? Can a vector be used as an iterator? If so what happens if the vector is mutated inside the iterator scope? Do we need to COW it?

Procedures returning vectors? Procedures taking vectors as arguments makes sense but can functions take vectors as arguments?

@rcunrau

rcunrau commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

I'm sorry, but I don't understand what your diagram is trying to say.

In general, vectors are pretty close to interchangeable with 1D arrays, but vectors of vectors are completely different than 2D arrays. For most math ops, vectors and arrays are compatible, I think. For parameters, slices of vectors can be passed as array arguments, but arrays cannot be passed as vector arguments.

vectors should be usable as iterators wherever arrays can be used as iterators. Like other languages, if you mutate the vector/array inside the loop, you can make bad things happen. We do not need to protect users from themselves. I suppose a good compiler would warn about yuckiness. It might also be fun to give some examples of programs stomping themselves.

Functions must be pure, so they should be able to take const vectors. I would think changing the size is a side effect, unless the vectors are passed by deep copy. The compilers can either check that a vector is not modified, or pass by deep copy and let function do what it wants. Which do you think is better?

@rcunrau

rcunrau commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

BTW, I cannot find anything anywhere that uses the term elaboration as you do. The C99 spec seems to call them "variable length arrays", and Wikipedia indicates that VLAs are in 10 different languages, including Ada. I have to admit I do not understand where this is coming from. Can you show me the email trail of confused students who couldn't comprehend the array sizing semantics of Gazprea?

@rcunrau

rcunrau commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

All that said, I think your writing is beautiful, and explains everything wonderfully. If you rewrite the rest of the spec to match this level of clarity it will be amazing :-)

@Sir-NoChill

Sir-NoChill commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

In terms of the diagram, here is my updated diagram after flushing this out for clarity and noting a few of my mistakes:

image

Legend

  • Lines -> type relations
  • Lines marked implicit -> type relations that can be assumed without explicit casting
  • Dotted lines -> generic rules.
  • Dotted containers -> informal type groupings

Scalars

image

This denotes the set of scalar (or primitive) types in gazprea. More formally, T is the 'top' of the lattice and gazprea has no 'bottom' of the lattice (which, in a language like dart or rust, is the never type (rust: !)).

So this reads that booleans, characters, integers and reals are each an individual component of the set of scalar types, with a one-way implicit conversion existing between integers and reals. It also notes that there is no type from which we can promote (implicitly) to get a boolean, character or integer (which would be null in previous versions of the spec).

[note] void

We may want to consider adding void to this lattice, but void is treated quite specially in other languages. Void is a scalar type representing the absence of any value, so it is a valid type, but not a valid variable assignment. If I recall correctly, dart has the never type because it explicitly can 'never be instantiated' so any program that attempts to create a never will immediately throw, whereas creating a void type (for example, calling a procedure that returns void) is legal, but assigning void to a variable is meaningless. You can consult dart's documentation on null-safety for a pretty good explanation here of why we might want to consider void as part of the type lattice.

So integers can automatically promote to reals but not the other way around and there are no other implicit scalar promotions.

Aggregate types

[note] Errata

I realised that I forgot to add in the Universe type (so equivalent to 'any'). The image below is the updated version including the universe, not the same as the original image in this PR.

image

Aggregate types here read that we can use any scalar type to construct an aggregate type of known, definite size.

  • Definite size - a size that is explicitly defined and known at runtime. A vector cannot fulfill this definition at runtime unless this is the first definition of a given vector variable and that given vector variable has an explicit size initializer. That means that a vector cannot be constructed directly from a scalar:
  • if we have a declaration like vector<integer> a = 1; this would be ill formed, throwing a size error (???)

The errata above notes that this should actually read that any type in the universe of types can be used to construct an aggregate type.

Two things that I should make clear:

  1. The string to char[n] implicit conversion is now outdated as of friday's discussions. I have accounted for this in the updated diagram. It notes that a char[n] array can be treated as a string which is a vector (???)
  2. The integer array to real array definition is implicit, any integer array can be treated like a real array implicitly if any of the actions require operations with a 'real' value.

Composite types

image

This is just a grouping of other, arbitrary types, so it just reads that a composite type is the composition of multiple instances of U. I have thus far dropped the requirement that a struct/tuple have more than one composing type, but that can be made explicit. I have this 'unit struct' type gated behind a feature flag in the solution compiler.

@Sir-NoChill
Sir-NoChill requested a review from rcunrau August 2, 2026 13:49
@Sir-NoChill

Copy link
Copy Markdown
Collaborator Author

As a note, I found VHDL and Verilog use elaboration to talk about the stage of expanding the source into usable gates, but you are right, none of the current language specifications define elaboration. I believe they all call this 'runtime', so our arrays would still be 'runtime' sized, but such that size is const... I don't really like that definition, to be discussed further.

@Sir-NoChill
Sir-NoChill requested a review from novo52 August 3, 2026 15:47
@novo52

novo52 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

There seem to be ambiguities surrounding the implicit casts between vectors, arrays, and strings, especially surrounding strings.

I'd resolve it by removing the implicit conversion from vector<T> to T[*] entirely, and exposing a vector's contents as an array through a method instead. Implicit conversions run one way only — array → vector and array → string; the reverse is always explicit, via .arr(). A vector then can't be used in any position where the ambiguity bites: no binary operations, no output. .arr() is an explicit l-value view, not a promotion, so passing it to a var parameter is plainly legal.

string fits the same shape: based on vector<character> in a semi-object-oriented sense — it converts implicitly to vector<character>, but is a separate type with its own behaviour (raw-text output, || producing a string, double-quoted literals), and exposes .arr() for its contents as character[*].

What you can do:

procedure add_one_elementwise(var integer[*] x);

procedure main() returns integer {
    var vector<integer> x1 = [1, 2, 3];      // implicit cast, integer[3] -> vector<integer>

    // .arr() aliases rather than copies, like a slice, so a var arg writes through
    call add_one_elementwise(x1.arr());
    x1.arr() -> std_output;                  // [2 3 4]

    integer[*] y = x1.arr();                 // assignment still deep-copies, as always

    var vector<integer[2]> x2 = [[1, 2], [3, 4]];
    integer[*][*] m = x2.arr();              // rank-2 array
    m -> std_output;                         // [[1 2] [3 4]]

    // strings work the same way
    var string s = "abcde";
    character[*] c = s.arr();                // ['a', 'b', 'c', 'd', 'e']
    vector<character> vc = s;                // implicit; string is based on vector<character>

    s -> std_output;                         // abcde
    s.arr() -> std_output;                   // [a b c d e]

    string t = s || "fg";                    // string, not character[*]
    string u = s[1..3];                      // slice is character[*]; the assignment copies it
                                             // into a fresh string, "ab"
    return 0;
}

What you can't do:

procedure add_elementwise(var integer[*] x, const integer[*] y);
procedure append_bang(var string s);

procedure main() returns integer {
    var vector<integer> x1 = [1, 2, 3];

    // AliasingError; both views are x1
    call add_elementwise(x1.arr(), x1.arr());
    x1 -> std_output;                        // TypeError; no implicit vector -> array
    integer[*] z = x1;                       // TypeError; needs x1.arr()
    integer[*] w = x1 + [1, 1, 1];           // TypeError; no binary operations on vectors

    var string s = "abcde";
    character[*] c = s;                      // TypeError; needs s.arr()
    call append_bang(s[1..3]);               // TypeError; a slice is character[*], not string

    return 0;
}

The aliasing case needs no new machinery: procedures.rst:213-217 already detects aliasing "whenever the same array is used, regardless of whether or not the access would overlap." The check just has to see through .arr().

Slicing always yields an array, for strings as much as for vectors. Recovering a string from a substring is just string u = s[1..3];, where the assignment deep-copies as usual.

This also settles what vector<character> prints, which vector.rst:48 and string.rst:6 currently disagree about: it doesn't. vec.arr() gives the bracketed form, string gives raw text.

The vector<integer[2]> case above also ties into the N-d array question in #101.

What do you two think?

@novo52

novo52 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

This is good work, the array-vector stuff is much more explict now. I worry that if this same approach is applied to everything in the spec, we may end up with a spec O(n^2) in size for n features, but IDK how much of a problem this actually is.

I'd like a rebase before we approve it finally; There have been changes since branching that are relevant to this.

@rcunrau

rcunrau commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

I think this is totally unnecessary. In the case where the argument is const and not var it does not mean you have to pass it by value. Just pass it by reference and don't allow stores. That's exactly how a const int* array works in C, where there is no such thing as passing arrays by value.

@Sir-NoChill

Copy link
Copy Markdown
Collaborator Author

@rcunrau when you say that this is unnecessary, is that because it seems obvious? I agree that there is no need to pass by value, but with MLIR it is easier to pretend like we are. Bufferization handles the actual lowering to pass by reference or pass by value, in general by reference as you said.

I'll do a rebase here, I am working on another set of fixes for the spec grammar and self consistency and then I'll fold this into that PR stack.

@novo52 Some questions regarding your comment:

removing the implicit conversion from vector to T[*] entirely

I don't think this exists after my revision, see this:

image

There is only an implicit upcast to a vector via initialization or from a char[*] to a string to a vector. I'm open to making that require an explicit cast.

Implicit conversions run one way only — array → vector and array → string; the reverse is always explicit, via .arr()

This provides a nice constraint to the type lattic (monotonicity), which will be nice for students, I will add that as a note.

it converts implicitly to vector

Does that mean we should say that string <-- implicit --> vector<character> ? I personally don't like that, it breaks the monotonicity constraint and it makes string essentially a typealias, which I think defeats the point of having a distinct string type. My thought is that if we promote from a string to a vector, the reverse should be explicit using an as<string>(...) construct.

x1 -> std_output; // TypeError; no implicit vector -> array

Does this mean vectors have no output semantics?

@rcunrau

rcunrau commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

I mean the arr method is not necessary because I don't actually see a problem that needs to be solved.
In c++ (or rust) when you define an object you also have to overload all the operators or write all the traits to make it work. In Gazprea the object is created by the compiler, so the compiler can do whatever it needs to make it work:

  • neither vectors nor arrays should be passed by value just because they're declared const. If the students want to pass by value it's not semantically wrong, but neither should the spec give any suggestion about implementation. If the spec says by-value today it is a bug and should be fixed.
  • vectors, strings, slices, and arrays can all be represented by a pointer and a length. That is why they are fundamentally interchangeable and no specific anything should be required to move between them.
  • v -> std_output is trivial to make work, and completely highlights my point that saying v.arr()->std_ouput solves a problem that never existed. What we have been saying is that a vector can be passed to an array argument by using a slice that is the entire vector, and that the argument v is equivalent to v[..].

@Sir-NoChill

Copy link
Copy Markdown
Collaborator Author

I tend to agree:

  • .arr() will not produce any information we could not obtain from the object directly, so bi-directional implicit casting seems acceptable to me. Kills the monotonicity of the type lattice a little, but I think you're right that the implementation won't really care. I do think that a cast vector<T> -> array<T> should require an explicit as<T>(U: from) conversion, but that makes the array -> vector case counter intuitive.
  • This is my motivation for suggesting that we move static vs. dynamic arrays into an analysis rather than a language construct, but that's probably not happening this term.
  • Agreed.

Sir-NoChill added a commit that referenced this pull request Aug 21, 2026
Open the type system to the settled "arbitrary nesting and arbitrary
structs" decision. Vectors, structs, tuples, and arrays may now hold any
storable element or field type, and fixed-size arrays generalize from the
two-dimensional matrix ceiling to arbitrary rank (T[n1]...[nk]). This
reverses the vector element-type restriction and the bans on nesting
structs/tuples inside structs and tuples.

A single authoritative rule lives at ssec:storable_types (types.rst):
everything except streams is storable; nesting is unbounded but must be
acyclic through value types; recursion is legal only through a vector,
the sole point of indirection. Each per-type page now defers to it, and
array.rst's element-type list is widened to match.

The >=2 field/element arity requirement is unchanged. Rank-agnostic
operations (a shape interface, n-d matrix multiply, broadcasting) are
left to a follow-up revision.

Refs: #132 #106 #82 #71 #101 #86
Assisted-by: Agent (claude) <ai@blobfish.icu>
Sir-NoChill added a commit that referenced this pull request Aug 21, 2026
Begin folding PR #106 into the consolidated spec, with review decisions.

- glossary: define `initialization` (renames #106's contested
  "elaboration") and `zero value` (RAII-const default; array padding).
- types/array.rst: new Sizing section and an Array-vs-Vector table, both
  stated for arrays of any rank per #138 -- not 2-D / base-type-only.
- Remove the `by` (stride) operator and `StrideError` entirely (it
  implies array views, which have no efficient implementation): the
  Stride operation, the precedence-table row, and the stride examples
  are gone.
- Concatenating two scalars is now a `TypeError`; at least one operand
  of `||` must be a composite value.

Refs #106.
Assisted-by: Agent (claude) <ai@blobfish.icu>
Sir-NoChill added a commit that referenced this pull request Aug 21, 2026
Renames the implicit-conversion vocabulary from 'promotion' to 'implicit cast' (heading of sec:typePromotion becomes 'Implicit Casts', label kept), and adds two-way array/vector casting sections. Part of folding #106.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Sir-NoChill added a commit that referenced this pull request Aug 21, 2026
string is now a language-supplied typealias for vector<character> (not a sub-type). Methods are defined as procedures with a self parameter; only vector/string have them. Adds ragged rules: vector<vector<T>> may be ragged, vector<T[*]> may not; no broadcasting or shape(). Folds #106.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Sir-NoChill added a commit that referenced this pull request Aug 21, 2026
Assignment sizing (array pads/SizeError vs vector replaces), generators always yield arrays, vectors print as arrays, length() on a vector is current-length. Replaces the shape() built-in with rows/columns. Folds #106.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Sir-NoChill added a commit that referenced this pull request Aug 21, 2026
Explicit-size array params are part of the signature; inferred [*] is initialized at the call; var array can't resize but var vector can. Single source-of-truth list of legal procedure-call positions; call results are castable. Folds #106.

Assisted-by: Agent (claude) <ai@blobfish.icu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants