I'm a big fan of Algol 68's treatment of variables. "Variables" are just constant references that point to memory allocated on the stack or on the heap. When you refer to a variable or a component of one, the result is of type "reference to T" instead of T directly; but values of type "reference to T" will be coerced into values of type T automatically. There is no need for an "address of" operator. In assignment and parameter passing, objects are copied (but a copy of a reference points to the same object as the original). Variable bindings always have dynamic extent (no closures); objects have indefinite or dynamic extent depending on whether they are heap- or stack-allocated. See the bottom of the post for more details about Algol 68.
I'm also a big fan of variables in Lisp and most dynamic languages. It's sometimes called "pass-by-sharing" or "pass-by-identity". Assignment and binding cause variables to hold the same object, instead of a copy. This essentially necessitates working with references to objects instead of objects directly. Bindings and objects have indefinite extent.
In my language, I have Lisp-style variables. Recently I've been contemplating adding references in order to increase its expressive power (e.g., the assignment operator can be described using the language itself*, since I have user-defined operators). However, I would want references to behave as in Algol 68 (or C++), with automatic dereferencing. But with Lisp-style variables, every binding has an implicit additional level of indirection, and the two ideas don't seem overly compatible.
Should I work the Lisp-variable-ness into the coercion process along with dereferencing? (Thereby adding one more level of indirection to Algol-style variables.) Is there prior art for that? Or an alternative to references that generalizes lvalues and expressions and allows the assignment operator to be defined.
I'm also not positive about how references interact with indefinite extent bindings and closures. Like, can a local variable escape its scope by assigning a reference to it to an outer variable? Should bindings have dynamic extent unless captured in a closure?
* A procedure to implement the operator would require support from the runtime/compiler, but its type can be expressed and lvalues can be consistently defined if the language has references.
Algol 68 is decently obscure these days, so here's some more information about how it deals with variables. Note that actual Algol 68 terminology is idiosyncratic and I'm "translating" it here to be more widely understood.
The simplest kind of declaration in Algol 68 is the "identity declaration", which corresponds roughly to a constant in other languages:
int i = 1;
This means that i is synonymous with the value of 1. More complicated example:
real circumference = 2 × 3.14 × r;
The right hand side of the = is evaluated and "ascribed" to the identifier on the left, so that they are equivalent in expressions. The computation of the value may involve side effects and need not be possible at compile time.
When an identifier has been introduced in an identity-declaration, it is conceptually replaced by its meaning every time it is applied. Re-declaring the same identifier creates a new binding.
To construct an object of a given type in Algol 68, you use a "generator", which looks like loc ⟨type⟩ or heap ⟨type⟩. Where the object is allocated is determined by whether you write "loc" (value is on the stack with dynamic extent) or "heap" (value is in heap with indefinite extent). The result of a generator is a value of type "reference-to-⟨type⟩", which is conceptually just a pointer to some part of the machine's memory. To express a reference type in Algol 68, write "ref ⟨type⟩".
Operations like array subscripting yields a reference, because it denotes a location in memory. Thus if you have an array A holding int, then A[2] would yield a value of type reference-to-int.
Algol 68's assignment operation is defined to operate on values of type "reference-to-t". For example,
(heap int) := 5
is like
*(malloc(sizeof(int))) = 5 in C; in both languages, it allocates space for an integer in the heap, stores 5 there, and promptly forgets about it.
Algol 68 assignment always copies the value.
As a consequence, you can get an int "variable" by writing
ref int i = loc int; i := 5
This means "declare i as constantly holding the value resulting from evaluating loc int, which will be of type reference-to-int; then store the value 5 in the memory location just ascribed to i. The value (a pointer) ascribed to i will not change as long as it is in scope; but the value i points to may vary. The assignment construct evaluates the left-hand-side to obtain a reference; there is no special case for when the destination of an assignment is an identifier.
Such notation is common enough that there's syntactic sugar (the initializer := 5 is optional):
loc int i := 5; heap int j := 5;
In fact, the loc or heap may be omitted, in order to make code look more like Algol 60.
All this is fine, but we still don't really have an "int variable". We have a constant reference-to-int. Theoretically, that means you'd have to dereference i anytime you don't want to assign to it (which is probably the more common case). Instead, Algol 68 defines a coercion from any type reference-to-⟨type⟩ to ⟨type⟩, which gets applied whenever necessary. In other words, dereferencing is automatic; in fact there is no way to dereference a value explicitly. Note that there is also no address-of operator, since anything we could meaningfully take the address of (variable names, array slices, fields of structures, etc.) already happens to yield a reference.
As a consequence of this system, classic problems are easily accounted for:
if p(x) then a else b fi := 10; proc swap integers(ref int a, b) void: (loc int t := a; a := b; b := t); loc int i := 1, j := 2; swap integers(i, j)
The first line is like *(p(x) ? &a : &b) in C.
A "pointer to a variable", rather than to a memory location created by a generator, is of type reference-to-reference-to-⟨type⟩. Another way to think about this is as "non-constant references".
loc int v := 10, w := 11; loc ref int p; p := v; print(p); p := w; print(p)
The assignments to p copy the references ascribed to v and w. Note that the declaration of p is equivalent to
ref ref int p = loc ref int;