context.remarkup
No OneTemporary

File Metadata

Created
Fri, Mar 13, 8:28 PM

context.remarkup

Data the applications operates with usually stored in memory explicitly 'as-is'.
In pursuit of reducing memory footprint many approaches are developed, such as storing data in packed format and unpacking on-the-fly when needed.
Extreme example of packing is procedural generation when only most basic parameters are stored in memory and actual data generated by predefined algorithms.

In Xreate context is any data(implicitly stored) which could be generated based on parameters reflecting place in the program code or,
to be more precise, place in the Control Flow Graph of program code, thereby context is data atatched to a code scope.

Static context

Static context is part of context fully known at the compile-time and thus use no memory for storing context data.

		import raw ("core/control-context.lp")	//needed to activate context reasoning
		case context::zero {			
			calculate = function::int {0}	//return 0
		}

		case context::one {
			calculate = function::int {1}	//return 1
		}

		test = function:: int; entry {
			context:: one.			//set up context, consisting of expression 'one'
			calculate() // return 1
		}

In this example at the moment of executing function calculate context (one) is fully defined statically(at compile time).
It represents case when information for decision of what specialization of calculate to choose requires no memory at all.

In example below we have only one function specialization:

		import raw ("core/control-context.lp")"

                case context:: safe {
			funSensitive = function:: int {0}
		}

		test = function:: int; entry {
			context:: safe.
			funcSensitive()
		}

There is only safe specialization of funSensitive and code compiles only if function called in expected context(safe in this example).

NOTE: : The example shows that function specialization guards could express requirements(or contract)to a context it works within.

Context propagation

Simplest case is a nested scope propagation. The term denotes context propagation from a parent scope to a nested one, or in other words, nested scope inherits parent scope's context.

		import raw ("core/control-context.lp")

		case context::safe {	//requires 'safe' context
			square = function(x:: int):: int {
				x * x
		}}

		test = function:: int; entry {
			// scopeA
			context:: safe; test.

			range = [1..10]:: [int].   
			loop fold(range->x::int, 0->acc):: int {  
				//scopeB
				acc + square(x)  // In the nested scope 'safe' context still accessable
			}
		}

The example demonstrates availability of a safe annotation in the nested context of code scope scopeB despite being declared in other scope.

More complicated case is interfunction context propagation. This stands for a context propagating through "caller/callee" relation: callee inherits caller context. The example below demonstates this:

		import raw ("core/control-context.lp")
		
		case context::toMillimeters {
			convertConcrete = function(source:: num)::num {
				10 * source 
			}
		}

		case context::toInches {
			convertConcrete = function(source:: num)::num {
				2 * source 
			}
		}

		convert= function(source:: num):: num { 
			convertConcrete(source) 
		} 

		test = function(source:: num):: num; entry {
			context:: toMillimeters.
			convert(1)
		}

To sort out what's going on here the control flow presented below:
{Fres/diagram-context-propagation-interfunction-static.dia}
There is only one path to reach convertConcrete and consequenlty context could be determind statically(on compile-time).
Since root function test sets up context toMillimeters only one relevant specialization of convertConcrete is vaild.

And now let's consider even more compilcated case with several different context pathes in CFG:
{Fres/diagram-context-propagation-interfunction-mixed, layout=right, float}

		import raw ("core/control-context.lp")
		compute = function::int {
			0
		}

		computeFast = function:: int {
			context:: computation(fast).

			compute()
		}

		computePrecisely = function:: int {
			context:: computation(precise). 

			compute()
		}

		test = function(cmnd:: int):: int; entry {
		   context:: arithmetic(iee754). 

		   if (cmnd > 0)::int {computePrecisely()} else {computeFast()} 
		}

Here several(that is two) pathes exists to reach function compute and moreover each path inroduces its own context.
Statically could be determined only path invariant context annotations or
annotations that are hold for any possible path to a given code scope.

In other words, statically determined context is a conjuction of all possible contexts of a given code scope.

Thus, in the given example only arithmetic(iee754) annotattion is statically determined for function compute
since it defined in the test function which is a root in the CFG(more generally, test dominates 'compute').

Context rules

Full utility of annotations is exposed if provided that we able to infer new annotations or decisions by employing rules of inference.
Given specific and somewhat compilcated nature of context annotations propagation there should be specific rules
that take in account these considerations.

To put it simple, context rules are rules to extend context by deriving new context annotations from already exisiting ones.

		import raw ("core/control-context.lp")
		case context:: toMilli {
			convert = function(lengthInCm::int)::int{
				10 * length
			}
		}

		main=function::int; entry {
			context:: output(milli).

			//here we inroduce context rule(aliasing)
			rule context::toMilli
			case output(milli) {true}

			convert(1)
		}

See syntax of annotation rules for details.
In this example obvious rule is inroduced having form like AnnotationOld -> AnnotationNew meaning aliasing to
inroduce AnnotationNew as new context annotation whenever AnnotationNew holds.

Host function main inroduces context output(milli) but callee requires slightly different context for execution(toMilli).
Thus, in order to adapt context the rule for introducing alias could be applied. As a consequence, context of both functions consists of
output(milli) and toMilli as well.

It is obvious, that rule is applicable to an annotations if it defined in the same scope(as in example above) or below, in terms of CFG.

Context rules propagation

Slightly more complex situation is for rules defined above annotations it applicable to.

		import raw ("core/control-context.lp")
		case context:: toMilli {
			convert = function(lengthInCm::int)::int{
				10 * length
			}
		}

		funcIntermediate = function(lengthInCm::int)::int{
			context:: output(milli).
			
			convert(1)
		}
		
		main=function::int; entry {
			//here we inroduce context rule(aliasing)
			rule context::toMilli
			case output(milli) {true}

			funcIntermediate(1)
		}

Here rule is defined in root function main but appropriate annotations for rule are defined in another function(funcIntermediate).
In other words, scope's context where rule is defined do not hold relevant annotations and therefore rule produces nothing new for the context.
Reasonable behaviour would be to inherit rules as it done for context annotations in order to apply rule whenever possible for nested scopes or for scopes located below in CFG.

Thus, unlike other rules a context rules implicilty propagated in the same way as it done for annotations.

Late context

Static scope context reasoning and consequent decisions is too weak to be usefull for real world applications, because it could deduce only context annotattions that are guarantied to be always true.
However, for any cases it's cruicial to consider possible contexts that is, contexts valid only under certain conditions.

		import raw ("core/control-context.lp")
		case context:: computation(fast) {
			compute = function:: num {
				0
			}
		}
		
		case context:: computation(precise) {
			compute = function:: num {
				0
			}
		}
		
		executeComputation= function:: num {
			compute()
		}

		test = function(cmnd:: num):: num; entry {
		   if (cmnd > 0)::num {
			context:: computation(fast).
			executeComputation()
			
		   } else {
			context:: computation(precise).
			executeComputation()
		   } 
		}

In this example, static(compile-time) reasoning is fruitless and infer nothing for compute scope
because there are two different pathes to reach compute from top function(test) and moreover each path conducts its own context:
computation(fast) through true block of IF statement and computation(precise) through false block respectively.

Such uncertainty could be resolved at runtime when one path or another is choosen.
It leads to a necessity of having late context - context data gathered on relevant occasion at runtime to determing right decisions.

To sum up, context consists of two complements parts: on the one hand static(early) context denotes compile time inferences,
and on the other hand, late(dynamic) context denotes annotations decided upon at runtime.

Remarks on late context implementation

To return to a last example, in order to correcly determine compute's context it's necessary:

  • to infer at compile-time every possible context for given scope
  • to store in memory key context data and fully restore particular context whenever needed

As of now, to convey late context data a hidden parameter injected to a function signature.
After such transformation signature of executeComputation looks like executeComputation(__hidden_context_data__),
where hidden_context_data holds data enough to determine within executeComputation which one of possible contexts it encountered with.
Consequently, executeComputation decides which specialization of compute should be called based on hidden_context_data value.

Only at run-time there is enough information for executeComputation to decide what specialization of compute to call.

NOTE: Since it is possible to determine number of possible contexts with diffent outcome decisions, it is possible to determine least size for late context data enough to identify each possible variant. (In example above, since there are only two specializons of compute, 1 bit is enough to convey late context data)

Path dependent late context

TODO: fill in this section

Context loops

TODO: fill in this section

See also:

Event Timeline