Initial type inference impl

This commit is contained in:
2026-09-08 18:52:47 +00:00
parent be53d3a5e5
commit 51bcac6225
5 changed files with 615 additions and 286 deletions
+76 -22
View File
@@ -34,16 +34,23 @@ DefineField both become DefineField on the fresh copy the function owns.
PARAMETERS -- the rule that keeps this general
====================================================================
A value the run USES but does not itself PRODUCE becomes a parameter, in
first-use order. Everything produced inside the run (Object, constants,
arithmetic that is wholly inside) moves into the function unchanged.
A run is handed anything it does not make for itself. Everything else moves
into the function unchanged. There are two kinds of parameter:
the object being mutated is used-but-not-produced -> it is a parameter.
a constant field value (Push1) is produced inside -> it moves in.
a field value from the enclosing frame (GetArg p) -> a parameter.
FRAME. GetArg, GetLoc, GetVarRef and their numbered forms are frame
reads. Each becomes a parameter, and the load stays at the call site.
RUN. A nested build inside this run. That run is lifted first, and its
call result is passed in. See NESTED BUILDS.
So a pure literal of constants lifts to a 0-parameter function; a mutation of
`a` lifts to a 1-parameter function; `a.x = p + 1` lifts to a 2-parameter one.
Everything else -- constants, arithmetic, field reads off a parameter -- is
made by the run and moves in.
Parameters are taken in first-use order. A frame slot appears once however
many times it is loaded, so a mutation's repeated loads of its object fold
into the one parameter, and the object is always parameter 0.
So a literal of constants lifts to a 0-parameter function; a mutation of `a`
lifts to a 1-parameter function; `a.x = p + 1` lifts to a 2-parameter one.
====================================================================
@@ -83,17 +90,32 @@ loads it with Fclosure8 Const(K).
DETECTION (one linear walk)
====================================================================
start a run at an Object, or at the first static-key write to an object.
extend it while the next instruction is one of:
Object (a nested object is its own run)
a value computation feeding a write in this run
DefineField, imm == Atom(F) (same object)
PutField, imm == Atom(F) (same object)
a (re)load of the same object
A run cannot be matched opcode by opcode, the way a class build can,
because <value> is any expression at all. FOLLOW THE OBJECT instead:
start a run at an Object (creation), or at a load of a slot that a
static-key write then writes to (mutation).
skip forward to the next instruction that USES the object.
carry on only while that use is a static-key write to it.
everything passed over on the way is the value computation, and it
joins the run.
A creation and a mutation differ only in how the object carries from one
write to the next:
DefineField hands the object back as a NEW value, so a creation run
is one value chain.
PutField hands nothing back, so a mutation re-loads the object before
every write.
END the run at any of:
the object is READ for anything but a field write -- passed to a call,
used in an expression, branched on.
a static-key write to a DIFFERENT object.
a jump, or an instruction some jump lands on. A run is ONE straight
line of code, so both sides of a branch are cut off. Without this
an `if` in the middle of a sequence of writes joins the two sides
into one run.
"Same object": a mutation re-loads the object before each write (each
GetArg0 / GetLocCheck is a fresh value), so sameness is judged by the load
@@ -103,6 +125,27 @@ DETECTION (one linear walk)
Ignore computed-key writes (DefineArrayEl / PutArrayEl) -- see extensions.
NESTED BUILDS: a run's own instructions are walked AGAIN, to find the
object builds inside it. Every Object in the program ends up in exactly
one lifted function, so one function describes one object shape.
{ x: { y: 1 }, z: { w: 2 } } lifts to THREE functions, not one.
An inner run is held INSIDE its parent's parameter list, as the Run
parameter it becomes. So lifting is depth-first: lift a run's Run
parameters, then the run itself, which needs their results.
The inner walk looks for BUILDS ONLY. A mutation writes its object over
and over, so a walk that looked for anything would match the run's own
second write as a run of its own:
a.x = 1 ; a.y = 2 one mutation run, NOT a run with one nested
ORDER IN THE PASS: demutate runs BEFORE declosure. declosure builds its
closure objects out of Object and DefineField Atom(int(k)), which a run
cannot tell apart from a user object. Run it after, and every closure gets
lifted as if it were an object literal.
====================================================================
WORKED EXAMPLE (test), real instructions
@@ -176,13 +219,24 @@ new FnById(Nm), name "<mutate>", arg_count 1:
FIELD VALUE FORMS -- all handled the same way
====================================================================
constant a.x = 1 Push1 moves into the function
argument { x: p } GetArg becomes a parameter
computed a.x = p + 1 p is a parameter; the Add moves in
field read a.x = b.y b is a parameter; the GetField moves in
nested object { x: { y: 1 } } the inner Object is its own run, lifted
first; its result is a value the outer
run uses -> a parameter of the outer
One rule covers them all: a frame read (GetArg, GetLoc, GetVarRef) becomes a
parameter and its load stays at the call site; everything else moves in.
a.x = 1 constant
Push1 is produced inside the run, so it moves in.
{ x: p } argument
GetArg is a frame read, so p becomes a parameter.
a.x = p + 1 computed
p is a parameter; the Add is produced inside, so it moves in.
a.x = b.y field read
b is a parameter; the GetField is produced inside, so it moves in.
{ x: { y: 1 } } nested object
The inner Object is a run of its OWN. It is lifted first, and the
outer run takes its result as a parameter. See NESTED BUILDS.
====================================================================