Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
Drop notion of Prim overloads have an embedded struct, does not auto-infer (yet).
PrimNodes default to EXTERNAL and not ALL structs.
Drop crossflt.  Ints & Flts now totally parallel, to keep their respective Clazzes separate.
Drop Call dying call, just use ctrl value.
Drop Call special live, just use normal live.
Drop extra arg to trial_resolve.
Drop extra class input to Field, since can refer to many classes
Fix bug TVStruct idx using find has wrong length.
Fix bug once_per at log 0.
  • Loading branch information
cliffclick committed Jun 23, 2023
1 parent 2b1faa4 commit 9f89d75
Show file tree
Hide file tree
Showing 23 changed files with 225 additions and 173 deletions.
51 changes: 39 additions & 12 deletions docs/variance.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ I use Lists as a stand-in for any collection. You may freely substitute Array
or Dict. I use leading Uppercase for Types, and lower case for variables and
values: '`List` vs `list`; `Int` vs `int`; `Str` vs `str`.

I use `<angle brackets>` for generic types: `List<Int>` vs `Ary<Str>`.
I use `<angle brackets>` for generic types: `List<Int>` and `Ary<Str>`.

There are interfaces, e.g. static structural typing (not duck typing, which is
the weakly typed runtime-only equivalent), and there are classes with
Expand Down Expand Up @@ -117,9 +117,12 @@ void doObjectExtract(Extractor<Object> extract) { Object o = extractor.first();
void doStringExtract(Extractor<String> extract) { String s = extractor.first(); }


Basically, it's a 2x2 matrix with one axis being consumer vs. producer (Logger vs. Extractor) and the other being base vs. derived (Object vs. String) for the type parameter of the generic type.
Basically, it's a 2x2 matrix with one axis being consumer vs. producer (Logger
vs. Extractor) and the other being base vs. derived (Object vs. String) for the
type parameter of the generic type.

Now let's create an implementation class that implements the example List type, which is both a Logger and Extractor:
Now let's create an implementation class that implements the example List type,
which is both a Logger and Extractor:

class ListImpl<T> implements List<T> {...}

Expand All @@ -140,8 +143,12 @@ doObjectExtracting(objList);
doObjectExtracting(strList);
doStringExtracting(objList);
doStringExtracting(strList);

Cameron - Yesterday at 10:25 AM
Let's start by eliminating the obviously correct ones, for which there is no debate, since they follow the type invariant model (which is always assumed to be correct):

Let's start by eliminating the obviously correct ones, for which there is no
debate, since they follow the type invariant model (which is always assumed to
be correct):

doObjectLogging(objList);
doStringLogging(strList);
Expand All @@ -159,10 +166,15 @@ That leaves only three variance cases to examine in detail. Let's explain the ea

doStringLogging(objList);

Here we have a function that logs strings to a logger, and a logger instance
that takes any object. A strictly type invariant language (a type system
without allowance for type variance) would disallow this, but it seems fairly
self-evident (and type safe) that a Logger<Object> can be used anywhere that a
Logger<String> is called for, and List<Object> is a Logger<Object>.

Here we have a function that logs strings to a logger, and a logger instance that takes any object. A strictly type invariant language (a type system without allowance for type variance) would disallow this, but it seems fairly self-evident (and type safe) that a Logger<Object> can be used anywhere that a Logger<String> is called for, and List<Object> is a Logger<Object>.

Almost identical is the ability to extract "any object" from a list of strings. The same logic applies here: Since a string is an object, it makes sense (and is type safe) to be able to extract objects from a list of strings:
Almost identical is the ability to extract "any object" from a list of
strings. The same logic applies here: Since a string is an object, it makes
sense (and is type safe) to be able to extract objects from a list of strings:

doObjectExtracting(strList);

Expand All @@ -172,16 +184,23 @@ That leaves just one problem case:
doObjectLogging(strList);


Common sense says that we cannot log objects to a logger that only takes strings, so at a surface level, common sense says that this type of variance should be disallowed.
Common sense says that we cannot log objects to a logger that only takes
strings, so at a surface level, common sense says that this type of variance
should be disallowed.

But there's a hidden gotcha in the example, and here it is: If a String is an Object, then can we also say that a List<String> is a List<Object>? And here we can see why some languages say "no!" -- because List both consumes and produces T, so a List<String> cannot be passed to a function as a List<Object> because the function may call the add method -- passing any Object and not just a String! -- on the underlying List<String>.
But there's a hidden gotcha in the example, and here it is: If a String is an
Object, then can we also say that a List<String> is a List<Object>? And here we
can see why some languages say "no!" -- because List both consumes and produces
T, so a List<String> cannot be passed to a function as a List<Object> because
the function may call the add method -- passing any Object and not just a
String! -- on the underlying List<String>.




I'd suggest that there is no "figuring out" to do here. It's not a right
vs. wrong question; rather, it is a decision made to either allow or disallow a
certain form of type variance, based on some fundamental principles. This is,
certain form of type variance, based on some fundamental principles. This is,
for example, the difference between Java arrays (a String[] "is a" Object[])
and Java collections (a List<String> is not a List<Object>) We call this "the
fourth quadrant problem". Here's an early design note on the topic:
Expand All @@ -204,5 +223,13 @@ Tying the numbers from the numbered quadrants to our example:
If number 3 is disallowed, then List<String> is not and cannot be a List<Object>.

If number 3 is allowed, then List<String> can be a List<Object>.
For the record, we were determined to make number 3 work, yet still with as much compile-time type safely and as few explicit casts as possible. And at this point, I think I can say that we achieved that.
(What I'm writing up here is the distillation of four different engineers working on this one problem for hundreds of -- and maybe even a thousand -- hours, so don't be surprised if it feels overwhelmingly complex. Among other things, I'm writing it down here and trying to explain it in a followable sequence as another way of trying to re-digest it myself.)

For the record, we were determined to make number 3 work, yet still with as
much compile-time type safely and as few explicit casts as possible. And at
this point, I think I can say that we achieved that.

(What I'm writing up here is the distillation of four different engineers
working on this one problem for hundreds of -- and maybe even a thousand --
hours, so don't be surprised if it feels overwhelmingly complex. Among other
things, I'm writing it down here and trying to explain it in a followable
sequence as another way of trying to re-digest it myself.)
2 changes: 1 addition & 1 deletion src/main/java/com/cliffc/aa/AA.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static <T> T p(T x, String s) {
private static int ASSERT_CNT;
public static boolean once_per() { return once_per(8); }
public static boolean once_per(int log) {
return (ASSERT_CNT++ & ((1L<<(log-1))-1))!=0;
return (ASSERT_CNT++ & ((1L<<log)-1))!=0;
}
static void reset() { ASSERT_CNT=0; }
}
10 changes: 3 additions & 7 deletions src/main/java/com/cliffc/aa/node/BindFPNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,12 @@ private Type bind(Type fun, Type dsp, boolean over) {
progress |= lam.dsp().unify(dsp().tvar(), test);
} else if( fptv instanceof TVLambda lam ) {
progress |= lam.dsp().unify(dsp().tvar(),test);
} else {
fptv.deps_add(this);
}
return progress;
}
private boolean _unify(Type t, TV3 tv, boolean test ) {
if( t instanceof TypeFunPtr && tv instanceof TVLambda lam ) {
return lam.dsp().unify(dsp().tvar(),test);
}
return false;
}


// Error to double-bind
@Override public ErrMsg err( boolean fast ) {
if( fp()._val instanceof TypeFunPtr tfp && tfp.has_dsp() &&
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/cliffc/aa/node/CallEpiNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public final class CallEpiNode extends Node {
public CallEpiNode( Node... nodes ) {
super(OP_CALLEPI,nodes);
Env.GVN.add_reduce(call());
Env.GVN.add_flow_defs(call()); // All call inputs liveness change
_live=RootNode.def_mem(this);
}
@Override public String xstr() {// Self short name
Expand Down
32 changes: 8 additions & 24 deletions src/main/java/com/cliffc/aa/node/CallNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -282,19 +282,6 @@ static public TypeFunPtr ttfp( Type t ) {
}
return null;
}

// During parse a Call will have no uses, no keep and no CEPI - because these
// are all being constructed; this Call is still alive. After Combo, a dying
// Call might also have no CEPI (with or without uses).
private boolean dying(Node def) {
if( in(0)==Env.XCTRL ) return true;
in(0).deps_add(this);
if( def!=null ) in(0).deps_add(def);
CallEpiNode cepi = cepi();
if( cepi!=null ) { cepi.deps_add(def) ; return false; }
if( _uses.find(GVNGCM.KEEP_ALIVE)!=-1 ) return false;
return Combo.post() || _uses._len>0; // Was wired once, so dying
}

// Pass thru all inputs directly - just a direct gather/scatter. The gather
// enforces SESE which in turn allows more precise memory and aliasing. The
Expand All @@ -308,7 +295,7 @@ private boolean dying(Node def) {
// Lift to ANY and let DCE remove final uses. Complex test is to avoid
// killing a Call under construction during Parse: will have no uses, no
// Keep and no cepi (for a tiny short time).
if( dying(this) ) return Type.ANY;
if( val(0)==Type.XCTRL || val(0)==Type.ANY ) return Type.ANY;

// Result type includes a type-per-input, plus one for the function
final Type[] ts = Types.get(_defs._len);
Expand Down Expand Up @@ -341,14 +328,6 @@ private boolean dying(Node def) {

return TypeTuple.make(ts);
}

@Override public Type live() {
if( _is_copy ) return _live;
CallEpiNode cepi = cepi();
if( cepi==null ) return dying(null) ? Type.ANY : RootNode.def_mem(null);
if( cepi._live==Type.ANY ) return Type.ANY;
return super.live(); // Ok, take liveness from all users
}

static final Type FP_LIVE = TypeStruct.UNUSED.add_fldx(TypeFld.make("fp",Type.ALL));
@Override public Type live_use( int i ) {
Expand Down Expand Up @@ -380,8 +359,13 @@ private boolean dying(Node def) {
// Check that all fidxs are wired. If not wired, a future wired fidx might
// use the call input. Post-Combo, all is wired, but dead Calls might be
// unwinding.
if( dying(def) ) return Type.ANY;
if( Combo.pre() && _uses.len()==1 ) return Type.ALL; // Not wired, assume the worst user
if( val(0)==Type.XCTRL ) return Type.ANY;
CallEpiNode cepi = cepi();
if( cepi==null ) {
deps_add(def);
return Type.ALL.oob(Combo.post());
}
if( !cepi.is_CG(true) ) return Type.ALL; // Not fully wired, assume the worse user yet to come

// Since wired, we can check all uses to see if this argument is alive.
Type t = Type.ANY;
Expand Down
19 changes: 9 additions & 10 deletions src/main/java/com/cliffc/aa/node/FieldNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public FieldNode(Node struct, String fld, boolean str, Parse bad) {
boolean lo = _tvar==null || Combo.HM_AMBI;
if( t instanceof TypeStruct ts )
return lo ? meet(ts) : join(ts);
return oob_ret(t);
return TypeNil.EXTERNAL;
}

// Field from Base looks in the base CLZ.
Expand All @@ -76,10 +76,7 @@ public FieldNode(Node struct, String fld, boolean str, Parse bad) {
StructNode clazz = clz_node(t);
if( clazz ==null ) return oob_ret(t);
tstr = clazz._val; // Value from clazz
// Add a dep edge to the clazz, so value changes propagate permanently
// TODO: Busted when mixing compatible classes, e.g. generic factorial with ints & flts.
if( len()==2 ) assert in(1)==clazz;
else add_def(clazz);
}
// Hit on a field
if( tstr instanceof TypeStruct ts ) {
Expand All @@ -89,8 +86,9 @@ public FieldNode(Node struct, String fld, boolean str, Parse bad) {
}
// Miss on closed structs looks at superclass.
// Miss on open structs dunno if the field will yet appear
if( ts._def==TypeStruct.ISUSED && Util.eq(ts.fld(0)._fld,".") )
throw unimpl();
if( ts.len()>=1 && Util.eq(ts.fld(0)._fld,".") )
throw unimpl();
return _str ? ts._def : ts.oob(Type.ALL);
}
return oob_ret(tstr);
}
Expand Down Expand Up @@ -229,7 +227,7 @@ private boolean do_fld( TV3 fld, boolean test ) {
return tvar().unify(fld,test);
}
private boolean do_resolve( TVStruct tstr, boolean test ) {
if( Combo.HM_AMBI ) return false; // Failed earlier, can never resolve
//if( Combo.HM_AMBI ) return false; // Failed earlier, can never resolve
boolean progress = try_resolve(tstr,test);
if( is_resolving() || test ) return progress; // Failed to resolve, or resolved but testing
// Known to be resolved and in RHS
Expand Down Expand Up @@ -263,7 +261,7 @@ private boolean try_resolve( TVStruct str, boolean test ) {
str.deps_add(this);
return false;
}
if( trial_resolve(true, tvar(), str, str, test) )
if( trial_resolve(true, tvar(), str, test) )
return true; // Resolve succeeded!
// No progress, try again if self changes
if( !test ) tvar().deps_add_deep(this);
Expand Down Expand Up @@ -326,9 +324,10 @@ public String resolve_failed_msg() {

public boolean resolve_ambiguous_msg() {
TV3 pattern = tvar();
TVStruct ts = tvar(0).as_struct();
TV3 tv0 = tvar(0);
TVStruct ts = tv0.as_struct();
boolean progress = ts.del_fld(_fld); // Do not push pinned uphill
return ts.unify_err("Ambiguous, matching choices % vs",pattern,_bad,false) | progress;
return tv0.unify_err("Ambiguous, matching choices % vs",pattern,_bad,false) | progress;
}

// True if ambiguous (more than one match), false if no matches.
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/cliffc/aa/node/FunNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,17 @@ private boolean use_is_wired(FunPtrNode fptr, Node use) {
}

@Override public Type live_use( int i ) {
if( in(0)==this ) return _live; // Dead self-copy
Node def = in(i);
if( def instanceof RootNode ) throw unimpl();
if( def instanceof ConNode ) return Type.ANY; // Dead control path
assert def.is_CFG();
if( def==this ) return Type.ANY; // Dead self-copy
assert def instanceof CallNode;
ParmNode pmem = parm(MEM_IDX);
if( pmem==null ) return TypeMem.ANYMEM; // No mem parm, so pure function
pmem.deps_add(def);
if( !(pmem._live instanceof TypeMem mem) ) return Type.ANY;
TypeMem mem2 = mem.remove(RootNode.KILL_ALIASES);
return mem2; // Pass through mem liveness
// Pass through mem liveness
return mem.remove(RootNode.KILL_ALIASES);
}

// ----
Expand Down
63 changes: 39 additions & 24 deletions src/main/java/com/cliffc/aa/node/LoadNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ public LoadNode( Node mem, Node adr, Parse bad ) {
Node mem() { return in(MEM_IDX); }
Node adr() { return in(DSP_IDX); }
private Node set_mem(Node a) { return set_def(MEM_IDX,a); }
//public TypeFld find(TypeStruct ts) { return ts.get(_fld); }

@Override public Type value() {
Type tadr = adr()._val;
Type tmem = mem()._val; // Memory
return switch(tadr ) {
case TypeMemPtr tmp -> // Loading from a pointer
tmem instanceof TypeMem tm ? tm.ld(tmp) : tmem.oob();
case TypeNil tn -> tn; // Load from prototype/primitive is a no-op
default -> tadr.oob(); // Nothing sane
};
Type tmem = mem()._val;
if( (tadr instanceof TypeStruct ts) )
return ts; // Happens if user directly calls an oper
if( !(tadr instanceof TypeNil ta) )
return tadr.oob(TypeStruct.ISUSED); // Not a address
if( !(tmem instanceof TypeMem tm) )
return tmem.oob(TypeStruct.ISUSED); // Not a memory
if( ta==TypeNil.NIL || ta==TypeNil.XNIL )
ta = (TypeNil)ta.meet(PrimNode.PINT._val);

return tm.ld(ta);
}

// The only memory required here is what is needed to support the Load.
Expand Down Expand Up @@ -75,16 +78,11 @@ public LoadNode( Node mem, Node adr, Parse bad ) {
// We allow Loads against structs to allow for nested (inlined) structs.
//if( tadr instanceof TypeStruct ) return adr();

// We allow Loads against unwrapped primitives, as part of the normal Oper
// expansion in the Parser. These are no-ops.
if( tadr instanceof TypeInt ) return adr;
if( tadr instanceof TypeFlt ) return adr;
if( tadr instanceof TypeStruct ) return adr; // Overload struct
// Dunno about other things than pointers
if( !(tadr instanceof TypeMemPtr tmp) ) return null;
if( !(tadr instanceof TypeNil tn) ) return null;
if( adr instanceof FreshNode frsh ) adr = frsh.id();
// If we can find an exact previous store, fold immediately to the value.
Node ps = find_previous_struct(this, mem(),adr,tmp._aliases);
Node ps = find_previous_struct(this, mem(),adr,tn._aliases);
if( ps instanceof StoreNode st ) {
Node rez = st.rez();
if( rez==null ) return null;
Expand Down Expand Up @@ -249,15 +247,32 @@ static Node find_previous_struct(Node ldst, Node mem, Node adr, BitsAlias aliase
}

@Override public boolean has_tvar() { return true; }
@Override public TV3 _set_tvar() {
// Self is just an open struct
TVStruct self = new TVStruct(true);
_tvar = self; // Stop cycles
TV3 adr = adr().set_tvar();
if( adr instanceof TVPtr ptr ) ptr.load().unify(self,false);
else adr.unify(new TVPtr(BitsAlias.EMPTY,self),false);
return (_tvar=self.find());
@Override public TV3 _set_tvar() { return new TVStruct(true); }

// All field loads might actually be against a pointer OR a plain struct. So
// we have to stall until the input decides it is either a ptr or struct. This
// is because fields can themselves hold embedded structs, and do so for the
// primitives in general. The parser cannot tell the difference from a
// load-from-ptr-then-field-extract vs just the field-extract part.
@Override public boolean unify( boolean test ) {
TV3 self = tvar();
TV3 tadr = adr().tvar();
if( tadr instanceof TVLeaf ) {
tadr.deps_add(this);
return false; // Stall until not a leaf
}
// Check for being a ptr
TVPtr ptr = null;
if( tadr instanceof TVErr err ) ptr = err.as_ptr();
if( tadr instanceof TVPtr ptr0) ptr = ptr0;
if( ptr!=null ) return ptr.load().unify(self,false);
// Check for being a struct
TVStruct ts = null;
if( tadr instanceof TVErr err ) ts = err.as_struct();
if( tadr instanceof TVStruct ts0) ts = ts0;
if( ts!=null ) return ts.unify(self,false);
// Some kind of error now
return tadr.unify(new TVPtr(BitsAlias.EMPTY,self.as_struct()),false);
}

@Override public boolean unify( boolean test ) { return false; }
}
4 changes: 1 addition & 3 deletions src/main/java/com/cliffc/aa/node/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -762,9 +762,7 @@ public boolean should_con(Type t) {
public final void walk_initype( VBitSet visit ) {
if( visit.tset(_uid) ) return; // Been there, done that
Env.GVN.add_flow(this); // On worklist and mark visited
if( has_tvar() ) { set_tvar();
//if( is_prim() ) tvar().set_widen();
}
if( has_tvar() ) set_tvar();
if( this instanceof FunNode fun )
fun.set_unknown_callers();
_val = _live = Type.ANY; // Highest value
Expand Down
Loading

0 comments on commit 9f89d75

Please sign in to comment.