Skip to content

Commit e46e670

Browse files
committed
New let stable; supercedes old simplify-on-state
- New grammar for `let stable` - Track bindings and resimplify if they become stable - Undo simplification if value later changes - Can use this to individually target stable state values to simplify on instead of tracking whole state change generations - Implements #80
1 parent 96d5a5f commit e46e670

File tree

11 files changed

+189
-82
lines changed

11 files changed

+189
-82
lines changed

atom/language-flitter/grammars/flitter.cson

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ patterns: [
7979
name: "keyword.control.flow.flitter"
8080
}
8181
{
82-
match: "\\b(if|elif|else|let|where)\\b"
82+
match: "\\b(if|elif|else|let|stable|where)\\b"
8383
name: "keyword.control.flow.flitter"
8484
}
8585
{

docs/language.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,32 @@ let x' = x + 1
706706
All identifiers may contain any number of single quote characters at the end.
707707
:::
708708

709+
### Stable let
710+
711+
The `let` keyword may be followed by the optional `stable` keyword to
712+
indicate bindings that are expected to be unchanging for long periods of time.
713+
When a stable let binding evaluates to the same value as it did in the last
714+
frame, the engine will re-simplify the program with the assumption that this
715+
value is now static. A run-time check will also be compiled in that verifies
716+
the value remains static. In the event that this check fails, execution will
717+
be abandoned and restarted with the original program.
718+
719+
For example:
720+
721+
```flitter
722+
let static SEED=time//30
723+
THINGS_COUNT=$:things_knob
724+
```
725+
726+
In cases where a value controls large portions of the program but changes
727+
infrequently, use of `let stable` can result in significant speed-ups. As the
728+
evaluated binding is always compared to the previous value, it is fine for the
729+
value to change continuously for short periods – for example, while turning a
730+
knob – as long as it then settles to a value that remains stable for a long
731+
period. Avoid using `let stable` for values that are stable for only short
732+
periods of time as the overhead of continuously re-simplifying the program
733+
may result in worse performance instead of better.
734+
709735
## Where
710736

711737
There is also an inline version of `let` known as `where`. This allows names to

src/flitter/engine/__main__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def main():
5050
parser.add_argument('--state', type=str, help="State save/restore file")
5151
parser.add_argument('--resetonswitch', action='store_true', default=False, help="Reset state when switching pages")
5252
parser.add_argument('--nosimplify', action='store_true', default=False, help="Disable the language simplifier")
53-
parser.add_argument('--simplifystate', type=convert_timecode_to_float, default=10, help="Simplify on state after stable period")
5453
parser.add_argument('--lockstep', action='store_true', default=False, help="Run clock in non-realtime mode")
5554
parser.add_argument('--define', '-D', action='append', default=[], type=keyvalue, dest='defines', help="Define name for evaluation")
5655
parser.add_argument('--vmstats', action='store_true', default=False, help="Report VM statistics")
@@ -69,7 +68,7 @@ def main():
6968
return os.execlp(sys.argv[0], *sys.argv)
7069
logger.info("Flitter version {}", __version__)
7170
controller = EngineController(target_fps=args.fps, screen=args.screen, fullscreen=args.fullscreen, vsync=args.vsync,
72-
state_file=args.state, reset_on_switch=args.resetonswitch, state_simplify_wait=args.simplifystate,
71+
state_file=args.state, reset_on_switch=args.resetonswitch,
7372
realtime=not args.lockstep, defined_names=dict(args.defines), vm_stats=args.vmstats,
7473
run_time=args.runtime, offscreen=args.offscreen, disable_simplifier=args.nosimplify,
7574
opengl_es=args.opengles, model_cache_time=args.modelcache)

src/flitter/engine/control.py

Lines changed: 26 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .. import setproctitle
1414
from ..cache import SharedCache
1515
from ..clock import BeatCounter, system_clock
16-
from ..language.vm import log_vm_stats
16+
from ..language.vm import log_vm_stats, ProgramInvalid
1717
from ..model import Vector, StateDict, Context, null, numbers_cache_counts, empty_numbers_cache
1818
from ..plugins import get_plugin
1919
from ..render.window.models import Model
@@ -24,7 +24,7 @@ class EngineController:
2424
MINIMUM_GC_INTERVAL = 10
2525

2626
def __init__(self, target_fps=60, screen=0, fullscreen=False, vsync=False, state_file=None,
27-
reset_on_switch=False, state_simplify_wait=0, realtime=True, defined_names=None, vm_stats=False,
27+
reset_on_switch=False, realtime=True, defined_names=None, vm_stats=False,
2828
run_time=None, offscreen=False, disable_simplifier=False, opengl_es=False, model_cache_time=300):
2929
self.default_fps = target_fps
3030
self.target_fps = target_fps
@@ -37,7 +37,6 @@ def __init__(self, target_fps=60, screen=0, fullscreen=False, vsync=False, state
3737
self.model_cache_time = model_cache_time
3838
self.reset_on_switch = reset_on_switch
3939
self.disable_simplifier = disable_simplifier
40-
self.state_simplify_wait = 0 if self.disable_simplifier else state_simplify_wait / 2
4140
if defined_names:
4241
self.defined_names = {key: Vector.coerce(value) for key, value in defined_names.items()}
4342
logger.trace("Defined names: {!r}", self.defined_names)
@@ -60,10 +59,6 @@ def __init__(self, target_fps=60, screen=0, fullscreen=False, vsync=False, state
6059
self.global_state = {}
6160
self.global_state_dirty = False
6261
self.state = None
63-
self.state_timestamp = None
64-
self.state_generation0 = None
65-
self.state_generation1 = None
66-
self.state_generation2 = None
6762
self.renderers = {}
6863
self.counter = BeatCounter()
6964
self.pages = []
@@ -89,10 +84,6 @@ def switch_to_page(self, page_number):
8984
self.state.clear_changed()
9085
path, state = self.pages[page_number]
9186
self.state = state
92-
self.state_timestamp = system_clock()
93-
self.state_generation0 = set()
94-
self.state_generation1 = set()
95-
self.state_generation2 = set()
9687
self.current_path = path
9788
self.current_page = page_number
9889
SharedCache.set_root(self.current_path)
@@ -178,7 +169,8 @@ async def run(self):
178169
gc_pending = False
179170
last_gc = None
180171
run_program = current_program = errors = logs = None
181-
simplify_state_time = frame_time + self.state_simplify_wait
172+
stables = set()
173+
stable_cache = {}
182174
static = dict(self.defined_names)
183175
static.update({'realtime': self.realtime, 'screen': self.screen, 'fullscreen': self.fullscreen,
184176
'vsync': self.vsync, 'offscreen': self.offscreen, 'opengl_es': self.opengl_es, 'run_time': self.run_time})
@@ -203,19 +195,20 @@ async def run(self):
203195
run_program = current_program = program
204196
self.handle_pragmas(program.pragmas, frame_time)
205197
errors = logs = None
206-
self.state_generation0 ^= self.state_generation1
207-
self.state_generation1 = self.state_generation2
208-
self.state_generation2 = set()
209-
simplify_state_time = frame_time + self.state_simplify_wait
210198

211199
now = system_clock()
212200
housekeeping += now
213201
execution -= now
214202

215203
if run_program is not None:
216204
context = Context(names={key: Vector.coerce(value) for key, value in dynamic.items()},
217-
state=self.state, references=self._references)
218-
run_program.run(context, record_stats=self.vm_stats)
205+
state=self.state, references=self._references, stables=stables, stable_cache=stable_cache)
206+
try:
207+
run_program.run(context, record_stats=self.vm_stats)
208+
except ProgramInvalid:
209+
logger.debug("Simplified program invalidated due to change of stable value")
210+
run_program = current_program
211+
run_program.run(context, record_stats=self.vm_stats)
219212
else:
220213
context = Context()
221214

@@ -240,50 +233,21 @@ async def run(self):
240233

241234
self.state['_counter'] = self.counter.tempo, self.counter.quantum, self.counter.start
242235

243-
if self.state.changed:
244-
if self.state_simplify_wait:
245-
changed_keys = self.state.changed_keys - self.state_generation0
246-
self.state_generation0 ^= changed_keys
247-
self.state_generation0 &= self.state.keys()
248-
if changed_keys:
249-
generation1to0 = self.state_generation1 & changed_keys
250-
changed_keys -= generation1to0
251-
self.state_generation1 -= generation1to0
252-
generation2to0 = self.state_generation2 & changed_keys
253-
if generation2to0:
254-
if run_program is not current_program:
255-
run_program = current_program
256-
logger.debug("Undo simplification on state; original program with {} instructions", len(run_program))
257-
self.state_generation1 ^= self.state_generation2 - generation2to0
258-
self.state_generation2 = set()
259-
simplify_state_time = frame_time + self.state_simplify_wait
260-
self.global_state_dirty = True
261-
self.state_timestamp = frame_time
262-
self.state.clear_changed()
263-
264-
if self.state_simplify_wait and frame_time > simplify_state_time:
265-
if current_program is not None and self.state_generation1:
266-
if self.state_generation1:
267-
self.state_generation2 ^= self.state_generation1
268-
simplify_state = self.state.with_keys(self.state_generation2)
269-
simplify_time = -system_clock()
270-
top = current_program.top.simplify(state=simplify_state, dynamic=dynamic, path=current_program.path)
271-
now = system_clock()
272-
simplify_time += now
273-
if top is current_program.top:
274-
logger.trace("Program unchanged after simplification on {} static state keys in {:.1f}ms",
275-
len(self.state_generation2), simplify_time*1000)
276-
else:
277-
compile_time = -now
278-
run_program = top.compile(initial_lnames=tuple(dynamic))
279-
run_program.set_path(current_program.path)
280-
run_program.set_top(top)
281-
compile_time += system_clock()
282-
logger.debug("Simplified on {} static state keys to {} instructions in {:.1f}/{:.1f}ms",
283-
len(self.state_generation2), len(run_program), simplify_time*1000, compile_time*1000)
284-
self.state_generation1 = self.state_generation0
285-
self.state_generation0 = set()
286-
simplify_state_time = frame_time + self.state_simplify_wait
236+
if not self.disable_simplifier and context.stables_changed and current_program is not None:
237+
simplify_time = -system_clock()
238+
top = current_program.top.simplify(dynamic=dynamic, path=current_program.path, stables=stables, stable_cache=stable_cache)
239+
now = system_clock()
240+
simplify_time += now
241+
if top is current_program.top:
242+
logger.trace("Program unchanged after simplification on stable values in {:.1f}ms", simplify_time*1000)
243+
else:
244+
compile_time = -now
245+
run_program = top.compile(initial_lnames=tuple(dynamic), stables=stables)
246+
run_program.set_path(current_program.path)
247+
run_program.set_top(top)
248+
compile_time += system_clock()
249+
logger.debug("Resimplified on stable values to {} instructions in {:.1f}/{:.1f}ms",
250+
len(run_program), simplify_time*1000, compile_time*1000)
287251

288252
if self.global_state_dirty and self.state_file is not None and frame_time > save_state_time:
289253
logger.trace("Saving state")

src/flitter/language/grammar.lark

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pragma : "%pragma" NAME literal _NL -> binding
2626
sequence : expression* let_expression?
2727

2828
let_expression : "let" multiline_bindings sequence -> let
29+
| "let" "stable" multiline_bindings sequence -> let_stable
2930
| "let" name_list "=" _NL _INDENT sequence _DEDENT sequence -> sequence_let
3031
| "func" NAME _LPAREN parameters _RPAREN _NL _INDENT sequence _DEDENT sequence -> function
3132
| "import" name_list "from" composition _NL sequence -> let_import

src/flitter/language/parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def sequence(self, *expressions):
146146
le = tree.LessThanOrEqualTo
147147
let = tree.Let
148148
let_import = tree.Import
149+
let_stable = tree.LetStable
149150
literal = tree.Literal
150151
logical_and = tree.And
151152
logical_not = tree.Not

src/flitter/language/tree.pyx

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ cdef bint sequence_pack(list expressions):
5454
cdef class Expression:
5555
cdef readonly frozenset unbound_names
5656

57-
def compile(self, tuple initial_lnames=(), set initial_errors=None, bint log_errors=True):
58-
cdef Program program = Program.__new__(Program, initial_lnames)
57+
def compile(self, tuple initial_lnames=(), set initial_errors=None, bint log_errors=True, set stables=None):
58+
cdef Program program = Program.__new__(Program, initial_lnames, stables)
5959
if initial_errors:
6060
program.compiler_errors.update(initial_errors)
6161
self._compile(program, list(initial_lnames))
@@ -65,7 +65,8 @@ cdef class Expression:
6565
logger.warning("Compiler error: {}", error)
6666
return program
6767

68-
def simplify(self, StateDict state=None, dict static=None, dynamic=None, Context parent=None, path=None, bint return_context=False):
68+
def simplify(self, StateDict state=None, dict static=None, dynamic=None, Context parent=None, path=None, bint return_context=False,
69+
set stables=None, dict stable_cache=None):
6970
cdef dict context_vars = {}
7071
cdef str key
7172
if static is not None:
@@ -74,7 +75,7 @@ cdef class Expression:
7475
if dynamic is not None:
7576
for key in dynamic:
7677
context_vars[key] = None
77-
cdef Context context = Context(state=state, names=context_vars)
78+
cdef Context context = Context(state=state, names=context_vars, stables=stables, stable_cache=stable_cache)
7879
context.path = path
7980
context.parent = parent
8081
cdef Expression expr = self
@@ -1412,7 +1413,7 @@ cdef class Let(Expression):
14121413
cdef int64_t i, j, n
14131414
cdef bint touched = False
14141415
cdef set shadowed=set(), discarded=set()
1415-
while type(body) is Let:
1416+
while type(body) is type(self):
14161417
bindings.extend((<Let>body).bindings)
14171418
body = (<Let>body).body
14181419
touched = True
@@ -1492,29 +1493,99 @@ cdef class Let(Expression):
14921493
break
14931494
else:
14941495
touched = True
1495-
if type(sbody) is Let:
1496+
if type(sbody) is type(self):
14961497
remaining.extend((<Let>sbody).bindings)
14971498
sbody = (<Let>sbody).body
14981499
resimplify = True
14991500
touched = True
15001501
if remaining:
15011502
if type(sbody) is Sequence and type((<Sequence>sbody).expressions[0]) is Literal:
15021503
if len((<Sequence>sbody).expressions) > 2:
1503-
sbody = Sequence(((<Sequence>sbody).expressions[0], Let(tuple(remaining), Sequence((<Sequence>sbody).expressions[1:]))))
1504+
sbody = Sequence(((<Sequence>sbody).expressions[0], type(self)(tuple(remaining), Sequence((<Sequence>sbody).expressions[1:]))))
15041505
else:
1505-
if type((<Sequence>sbody).expressions[1]) is Let:
1506+
if type((<Sequence>sbody).expressions[1]) is type(self):
15061507
resimplify = True
1507-
sbody = Sequence(((<Sequence>sbody).expressions[0], Let(tuple(remaining), (<Sequence>sbody).expressions[1])))
1508+
sbody = Sequence(((<Sequence>sbody).expressions[0], type(self)(tuple(remaining), (<Sequence>sbody).expressions[1])))
15081509
elif touched:
1509-
sbody = Let(tuple(remaining), sbody)
1510+
sbody = type(self)(tuple(remaining), sbody)
15101511
else:
15111512
return self
15121513
if resimplify:
15131514
return sbody._simplify(context)
15141515
return sbody
15151516

15161517
def __repr__(self):
1517-
return f'Let({self.bindings!r}, {self.body!r})'
1518+
return f'{type(self).__name__}({self.bindings!r}, {self.body!r})'
1519+
1520+
1521+
cdef int StableId = 0
1522+
1523+
cdef class LetStable(Let):
1524+
cdef readonly int id
1525+
1526+
def __init__(self, tuple bindings, Expression body):
1527+
global StableId
1528+
Let.__init__(self, bindings, body)
1529+
self.id = StableId
1530+
StableId += 1
1531+
1532+
cdef void _compile(self, Program program, list lnames):
1533+
cdef PolyBinding binding
1534+
cdef int64_t n=len(lnames)
1535+
cdef tuple key
1536+
for i, binding in enumerate(self.bindings):
1537+
key = (self.id, i)
1538+
binding.expr._compile(program, lnames)
1539+
if key in program.stables:
1540+
program.stable_assert(key)
1541+
else:
1542+
program.stable_test(key)
1543+
program.local_push(len(binding.names))
1544+
lnames.extend(binding.names)
1545+
self.body._compile(program, lnames)
1546+
cdef Instruction instr, compose=None
1547+
if len(lnames) > n:
1548+
if program.last_instruction().code == OpCode.Compose:
1549+
compose = program.pop_instruction()
1550+
instr = program.last_instruction()
1551+
if instr.code == OpCode.LocalDrop:
1552+
program.pop_instruction()
1553+
program.local_drop((<InstructionInt>instr).value + len(lnames) - n)
1554+
else:
1555+
program.local_drop(len(lnames) - n)
1556+
if compose is not None:
1557+
program.push_instruction(compose)
1558+
while len(lnames) > n:
1559+
lnames.pop()
1560+
1561+
cdef Expression _simplify(self, Context context):
1562+
cdef Expression body, expr = Let._simplify(self, context)
1563+
if type(expr) is not LetStable:
1564+
return expr
1565+
cdef LetStable stable = <LetStable>expr
1566+
cdef Vector value
1567+
cdef tuple key
1568+
cdef bint resimplify = False
1569+
cdef dict saved = context.names
1570+
context.names = saved.copy()
1571+
for i, binding in enumerate(stable.bindings):
1572+
key = (self.id, i)
1573+
if key in context.stables:
1574+
resimplify = True
1575+
value = context.stable_cache[key]
1576+
if len(binding.names) == 1:
1577+
name = <str>binding.names[0]
1578+
context.names[name] = value
1579+
else:
1580+
for j, name in enumerate(binding.names):
1581+
context.names[name] = value.item(j)
1582+
if resimplify:
1583+
body = stable.body._simplify(context)
1584+
if body is not stable.body:
1585+
stable = LetStable(stable.bindings, body)
1586+
context.names = saved
1587+
stable.id = self.id
1588+
return stable
15181589

15191590

15201591
cdef class For(Expression):

0 commit comments

Comments
 (0)