diff --git a/src/apps/cif2bcif/converter.ts b/src/apps/cif2bcif/converter.ts
index b663a301e60b8a7119875dbfac6b9fd3e850efaa..1a3a08221bdb79e9808e69dad4651aa141e2cd21 100644
--- a/src/apps/cif2bcif/converter.ts
+++ b/src/apps/cif2bcif/converter.ts
@@ -9,10 +9,11 @@ import CIF, { Category } from 'mol-io/reader/cif'
 import * as Encoder from 'mol-io/writer/cif'
 import * as fs from 'fs'
 import classify from './field-classifier'
+import { Run } from 'mol-task'
 
 async function getCIF(path: string) {
     const str = fs.readFileSync(path, 'utf8');
-    const parsed = await CIF.parseText(str)();
+    const parsed = await Run(CIF.parseText(str));
     if (parsed.isError) {
         throw new Error(parsed.toString());
     }
diff --git a/src/apps/combine-mmcif/index.ts b/src/apps/combine-mmcif/index.ts
index dd13d652deb5bebb6cbc153ed86bdf8d061ac8cd..d107f81132f4110c1c4d01152711ba7f8a9155c5 100644
--- a/src/apps/combine-mmcif/index.ts
+++ b/src/apps/combine-mmcif/index.ts
@@ -13,11 +13,11 @@ require('util.promisify').shim();
 const readFile = util.promisify(fs.readFile);
 const writeFile = util.promisify(fs.writeFile);
 
+import { Run, Progress } from 'mol-task'
 import { Database, Table, DatabaseCollection } from 'mol-data/db'
 import CIF from 'mol-io/reader/cif'
 // import { CCD_Schema } from 'mol-io/reader/cif/schema/ccd'
 import * as Encoder from 'mol-io/writer/cif'
-import Computation from 'mol-util/computation'
 import { mmCIF_Schema, mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
 import { CCD_Schema } from 'mol-io/reader/cif/schema/ccd';
 import { BIRD_Schema } from 'mol-io/reader/cif/schema/bird';
@@ -48,10 +48,6 @@ export async function ensureDataAvailable() {
     await ensureAvailable(BIRD_PATH, BIRD_URL)
 }
 
-function showProgress(tag: string, p: Computation.Progress) {
-    console.log(`[${tag}] ${p.message} ${p.isIndeterminate ? '' : (p.current / p.max * 100).toFixed(2) + '% '}(${p.elapsedMs | 0}ms)`)
-}
-
 export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
     const parsed = await parseCif(await readFile(path, 'utf8'))
     return CIF.toDatabaseCollection(schema, parsed.result)
@@ -80,14 +76,10 @@ export async function getBIRD() {
 }
 
 async function parseCif(data: string|Uint8Array) {
-    const comp = CIF.parse(data)
-    const ctx = Computation.observable({
-        updateRateMs: 250,
-        observer: p => showProgress(`cif parser ${typeof data === 'string' ? 'string' : 'binary'}`, p)
-    });
-    console.time('parse cif')
-    const parsed = await comp(ctx);
-    console.timeEnd('parse cif')
+    const comp = CIF.parse(data);
+    console.time('parse cif');
+    const parsed = await Run(comp, p => console.log(Progress.format(p)), 250);
+    console.timeEnd('parse cif');
     if (parsed.isError) throw parsed;
     return parsed
 }
diff --git a/src/apps/schema-generator/schema-from-mmcif-dic.ts b/src/apps/schema-generator/schema-from-mmcif-dic.ts
index 14859b34217695e44b43084a672debc175984e86..711fa67749f3e636f840f7c7e5505d9cfaa141f7 100644
--- a/src/apps/schema-generator/schema-from-mmcif-dic.ts
+++ b/src/apps/schema-generator/schema-from-mmcif-dic.ts
@@ -14,11 +14,12 @@ import CIF from 'mol-io/reader/cif'
 import { generateSchema } from './util/cif-dic'
 import { generate } from './util/generate'
 import { Filter, mergeFilters } from './util/json-schema'
+import { Run } from 'mol-task';
 
 async function runGenerateSchema(name: string, fieldNamesPath?: string, minCount = 0, typescript = false, out?: string) {
     await ensureMmcifDicAvailable()
     const comp = CIF.parseText(fs.readFileSync(MMCIF_DIC_PATH, 'utf8'))
-    const parsed = await comp();
+    const parsed = await Run(comp);
     if (parsed.isError) throw parsed
 
     // console.log(fieldNamesPath, minCount)
@@ -47,7 +48,7 @@ async function runGenerateSchema(name: string, fieldNamesPath?: string, minCount
 
 async function getFieldNamesFilter(fieldNamesPath: string): Promise<Filter> {
     const fieldNamesStr = fs.readFileSync(fieldNamesPath, 'utf8')
-    const parsed = await Csv(fieldNamesStr, { noColumnNames: true })();
+    const parsed = await Run(Csv(fieldNamesStr, { noColumnNames: true }));
     if (parsed.isError) throw parser.error
     const csvFile = parsed.result;
 
@@ -68,7 +69,7 @@ async function getFieldNamesFilter(fieldNamesPath: string): Promise<Filter> {
 
 async function getUsageCountsFilter(minCount: number): Promise<Filter> {
     const usageCountsStr = fs.readFileSync(MMCIF_USAGE_COUNTS_PATH, 'utf8')
-    const parsed = await Csv(usageCountsStr, { delimiter: ' ' })();
+    const parsed = await Run(Csv(usageCountsStr, { delimiter: ' ' }));
     if (parsed.isError) throw parser.error
     const csvFile = parsed.result;
 
diff --git a/src/apps/structure-info/index.ts b/src/apps/structure-info/index.ts
index 59f4664f3c050637cac2f418c30168da6cba26cc..7a911acb48122191c26689017d9fdde7ea2f5857 100644
--- a/src/apps/structure-info/index.ts
+++ b/src/apps/structure-info/index.ts
@@ -10,20 +10,12 @@ require('util.promisify').shim();
 
 // import { Table } from 'mol-data/db'
 import CIF from 'mol-io/reader/cif'
-import Computation from 'mol-util/computation'
 import { Model } from 'mol-model/structure'
-
-function showProgress(tag: string, p: Computation.Progress) {
-    console.log(`[${tag}] ${p.message} ${p.isIndeterminate ? '' : (p.current / p.max * 100).toFixed(2) + '% '}(${p.elapsedMs | 0}ms)`)
-}
+import { Run, Progress } from 'mol-task'
 
 async function parseCif(data: string|Uint8Array) {
-    const comp = CIF.parse(data)
-    const ctx = Computation.observable({
-        updateRateMs: 250,
-        observer: p => showProgress(`cif parser ${typeof data === 'string' ? 'string' : 'binary'}`, p)
-    });
-    const parsed = await comp(ctx);
+    const comp = CIF.parse(data);
+    const parsed = await Run(comp, p => console.log(Progress.format(p)), 250);
     if (parsed.isError) throw parsed;
     return parsed
 }
diff --git a/src/examples/computation.ts b/src/examples/task.ts
similarity index 54%
rename from src/examples/computation.ts
rename to src/examples/task.ts
index 8068bff3191764eacce29e8cc12b56377410daee..41025f042c48eb1252f14483ba94e1f10c771470 100644
--- a/src/examples/computation.ts
+++ b/src/examples/task.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Task, Run, Progress, Scheduler, now } from 'mol-task'
+import { Task, Run, Progress, Scheduler, now, MultistepTask, chunkedSubtask } from 'mol-task'
 
 export async function test1() {
     const t = Task.create('test', async () => 1);
@@ -13,11 +13,16 @@ export async function test1() {
 }
 
 function messageTree(root: Progress.Node, prefix = ''): string {
-    if (!root.children.length) return `${prefix}${root.progress.taskName}: ${root.progress.message}`;
+    const p = root.progress;
+    if (!root.children.length) {
+        if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}`;
+        return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}`;
+    }
 
     const newPrefix = prefix + '  |_ ';
     const subTree = root.children.map(c => messageTree(c, newPrefix));
-    return `${prefix}${root.progress.taskName}: ${root.progress.message}\n${subTree.join('\n')}`;
+    if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}\n${subTree.join('\n')}`;
+    return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}\n${subTree.join('\n')}`;
 }
 
 function createTask<T>(delayMs: number, r: T): Task<T> {
@@ -38,7 +43,7 @@ export function abortAfter(delay: number) {
     });
 }
 
-function testTree() {
+export function testTree() {
     return Task.create('test o', async ctx => {
         await Scheduler.delay(250);
         if (ctx.shouldUpdate) await ctx.update({ message: 'hi! 1' });
@@ -60,6 +65,40 @@ function testTree() {
     });
 }
 
+export type ChunkedState = { i: number, current: number, total: number }
+
+function processChunk(n: number, state: ChunkedState): number {
+    const toProcess = Math.min(state.current + n, state.total);
+    const start = state.current;
+    for (let i = start; i < toProcess; i++) {
+        for (let j = 0; j < 1000000; j++) {
+            state.i += (i * j + 1 + state.i) % 1023;
+            state.i = state.i % 1000;
+        }
+    }
+    state.current = toProcess;
+    return toProcess - start;
+}
+
+export const ms = MultistepTask('ms-task', ['step 1', 'step 2', 'step 3'], async (p: { i: number }, step, ctx) => {
+    await step(0);
+
+    const child = Task.create('chunked', async ctx => {
+        const s = await chunkedSubtask(ctx, 25, { i: 0, current: 0, total: 125 }, processChunk, (ctx, s, p) => ctx.update('chunk test ' + p))
+        return s.i;
+    });
+    
+    await ctx.runChild(child);
+    await Scheduler.delay(250);
+    await step(1);
+    await chunkedSubtask(ctx, 25, { i: 0, current: 0, total: 80 }, processChunk, (ctx, s, p) => ctx.update('chunk test ' + p))
+    await Scheduler.delay(250);
+    await step(2);
+    await Scheduler.delay(250);
+    return p.i + 3;
+})
+
+
 export function abortingObserver(p: Progress) {
     console.log(messageTree(p.root));
     if (now() - p.root.progress.startedTime > 1000) {
@@ -67,11 +106,16 @@ export function abortingObserver(p: Progress) {
     }
 }
 
+export function logP(p: Progress) { console.log(messageTree(p.root)); }
+
 async function test() {
     try {
         //const r = await Run(testTree(), p => console.log(messageTree(p.root)), 250);
-        const r = await Run(testTree(), abortingObserver, 250);
-        console.log(r);
+        //const r = await Run(testTree(), abortingObserver, 250);
+        //console.log(r);
+
+        const m = await Run(ms({ i: 10 }), logP);
+        console.log(m);
     } catch (e) {
         console.error(e);
     }
diff --git a/src/mol-io/reader/_spec/csv.spec.ts b/src/mol-io/reader/_spec/csv.spec.ts
index cd14e30e425101170e3a4d3c48a5d530fd6c9e5c..b074b5bc7d51a2df190180065852e4f7dec8c75d 100644
--- a/src/mol-io/reader/_spec/csv.spec.ts
+++ b/src/mol-io/reader/_spec/csv.spec.ts
@@ -5,6 +5,7 @@
  */
 
 import Csv from '../csv/parser'
+import { Run } from 'mol-task';
 
 const csvStringBasic = `StrCol,IntCol,FloatCol
 # comment
@@ -23,7 +24,7 @@ string2\t42\t2.44`
 
 describe('csv reader', () => {
     it('basic', async () => {
-        const parsed = await Csv(csvStringBasic)();
+        const parsed = await Run(Csv(csvStringBasic));
         if (parsed.isError) return;
         const csvFile = parsed.result;
 
@@ -45,7 +46,7 @@ describe('csv reader', () => {
     });
 
     it('advanced', async () => {
-        const parsed = await Csv(csvStringAdvanced)();
+        const parsed = await Run(Csv(csvStringAdvanced));
         if (parsed.isError) return;
         const csvFile = parsed.result;
 
@@ -62,7 +63,7 @@ describe('csv reader', () => {
     });
 
     it('tabs', async () => {
-        const parsed = await Csv(tabString, { delimiter: '\t' })();
+        const parsed = await Run(Csv(tabString, { delimiter: '\t' }));
         if (parsed.isError) return;
         const csvFile = parsed.result;
 
diff --git a/src/mol-io/reader/_spec/gro.spec.ts b/src/mol-io/reader/_spec/gro.spec.ts
index 888f23c83369fa8fc27e4adec486aab86003e7ab..6254be8f9d45d287f643e2d50993e8b62eb11bbe 100644
--- a/src/mol-io/reader/_spec/gro.spec.ts
+++ b/src/mol-io/reader/_spec/gro.spec.ts
@@ -6,6 +6,7 @@
  */
 
 import Gro from '../gro/parser'
+import { Run } from 'mol-task';
 
 const groString = `MD of 2 waters, t= 4.2
     6
@@ -26,7 +27,7 @@ const groStringHighPrecision = `Generated by trjconv : 2168 system t=  15.00000
 
 describe('gro reader', () => {
     it('basic', async () => {
-        const parsed = await Gro(groString)();
+        const parsed = await Run(Gro(groString));
 
         if (parsed.isError) {
             console.log(parsed)
@@ -57,7 +58,7 @@ describe('gro reader', () => {
     });
 
     it('high precision', async () => {
-        const parsed = await Gro(groStringHighPrecision)()
+        const parsed = await Run(Gro(groStringHighPrecision));
 
         if (parsed.isError) {
             console.log(parsed)
diff --git a/src/mol-io/reader/_spec/mol2.spec.ts b/src/mol-io/reader/_spec/mol2.spec.ts
index c329f61a4e5c66a28588d8424eb4327dbafb25ff..43244780636af6fbcd75010522ba9405b75ca2ff 100644
--- a/src/mol-io/reader/_spec/mol2.spec.ts
+++ b/src/mol-io/reader/_spec/mol2.spec.ts
@@ -1,5 +1,6 @@
 
 import Mol2 from '../mol2/parser'
+import { Run } from 'mol-task';
 
 const Mol2String = `@<TRIPOS>MOLECULE
 5816
@@ -246,7 +247,7 @@ GASTEIGER
 
 describe('mol2 reader', () => {
     it('basic', async () => {
-        const parsed =  await Mol2(Mol2String)();
+        const parsed =  await Run(Mol2(Mol2String));
         if (parsed.isError) {
             throw new Error(parsed.message);
         }
@@ -297,7 +298,7 @@ describe('mol2 reader', () => {
     });
 
     it('multiblocks', async () => {
-        const parsed =  await Mol2(Mol2StringMultiBlocks)();
+        const parsed =  await Run(Mol2(Mol2StringMultiBlocks));
         if (parsed.isError) {
             throw new Error(parsed.message);
         }
@@ -348,7 +349,7 @@ describe('mol2 reader', () => {
     });
 
     it('minimal', async () => {
-        const parsed =  await Mol2(Mol2StringMinimal)();
+        const parsed =  await Run(Mol2(Mol2StringMinimal));
         if (parsed.isError) {
             throw new Error(parsed.message);
         }
diff --git a/src/mol-io/reader/cif/binary/parser.ts b/src/mol-io/reader/cif/binary/parser.ts
index 4f1b705eb62810531f2d5665def1b88b189e82d4..bd53cd0ad2d569049d6145eb35481bb3187e1fae 100644
--- a/src/mol-io/reader/cif/binary/parser.ts
+++ b/src/mol-io/reader/cif/binary/parser.ts
@@ -9,7 +9,7 @@ import { EncodedCategory, EncodedFile } from '../../../common/binary-cif'
 import Field from './field'
 import Result from '../../result'
 import decodeMsgPack from '../../../common/msgpack/decode'
-import Computation from 'mol-util/computation'
+import { Task } from 'mol-task'
 
 function checkVersions(min: number[], current: number[]) {
     for (let i = 0; i < 2; i++) {
@@ -37,7 +37,7 @@ function Category(data: EncodedCategory): Data.Category {
 }
 
 export default function parse(data: Uint8Array) {
-    return Computation.create<Result<Data.File>>(async ctx => {
+    return Task.create<Result<Data.File>>('Parse BinaryCIF', async ctx => {
         const minVersion = [0, 3];
 
         try {
diff --git a/src/mol-io/reader/cif/text/parser.ts b/src/mol-io/reader/cif/text/parser.ts
index 8f6a466e7f7161692d898d1c5825449e42090df8..bbd4b39c83edaaa82b95b720511b9fc0baac7cb1 100644
--- a/src/mol-io/reader/cif/text/parser.ts
+++ b/src/mol-io/reader/cif/text/parser.ts
@@ -26,7 +26,7 @@ import * as Data from '../data-model'
 import Field from './field'
 import { Tokens, TokenBuilder } from '../../common/text/tokenizer'
 import Result from '../../result'
-import Computation from 'mol-util/computation'
+import { Task, RuntimeContext, chunkedSubtask } from 'mol-task'
 
 /**
  * Types of supported mmCIF tokens.
@@ -42,18 +42,18 @@ const enum CifTokenType {
 }
 
 interface TokenizerState {
-    data: string;
+    data: string,
 
-    position: number;
-    length: number;
-    isEscaped: boolean;
+    position: number,
+    length: number,
+    isEscaped: boolean,
 
-    lineNumber: number;
-    tokenType: CifTokenType;
-    tokenStart: number;
-    tokenEnd: number;
+    lineNumber: number,
+    tokenType: CifTokenType,
+    tokenStart: number,
+    tokenEnd: number,
 
-    chunker: Computation.Chunker
+    runtimeCtx: RuntimeContext
 }
 
 /**
@@ -387,7 +387,7 @@ function moveNext(state: TokenizerState) {
     while (state.tokenType === CifTokenType.Comment) moveNextInternal(state);
 }
 
-function createTokenizer(data: string, ctx: Computation.Context): TokenizerState {
+function createTokenizer(data: string, runtimeCtx: RuntimeContext): TokenizerState {
     return {
         data,
         length: data.length,
@@ -398,7 +398,7 @@ function createTokenizer(data: string, ctx: Computation.Context): TokenizerState
         lineNumber: 1,
         isEscaped: false,
 
-        chunker: Computation.chunker(ctx, 1000000)
+        runtimeCtx
     };
 }
 
@@ -468,7 +468,7 @@ interface LoopReadState {
     tokenCount: number
 }
 
-function readLoopChunk(state: LoopReadState, chunkSize: number) {
+function readLoopChunk(chunkSize: number, state: LoopReadState) {
     const { tokenizer, tokens, fieldCount } = state;
     let tokenCount = state.tokenCount;
     let counter = 0;
@@ -481,12 +481,14 @@ function readLoopChunk(state: LoopReadState, chunkSize: number) {
     return counter;
 }
 
-function readLoopChunks(state: LoopReadState) {
-    return state.tokenizer.chunker.process(
-        chunkSize => readLoopChunk(state, chunkSize),
-        update => update({ message: 'Parsing...', current: state.tokenizer.position, max: state.tokenizer.data.length }));
+function updateLoopChunk(ctx: RuntimeContext, state: LoopReadState) {
+    return ctx.update({ message: 'Parsing...', current: state.tokenizer.position, max: state.tokenizer.data.length });
 }
 
+// const readLoopChunks = ChunkedSubtask(1000000,
+//     (size, state: LoopReadState) => readLoopChunk(state, size),
+//     (ctx, state) => ctx.update({ message: 'Parsing...', current: state.tokenizer.position, max: state.tokenizer.data.length }));
+
 /**
  * Reads a loop.
  */
@@ -514,7 +516,7 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise
         tokens
     };
 
-    await readLoopChunks(state);
+    await chunkedSubtask(tokenizer.runtimeCtx, 1000000, state, readLoopChunk, updateLoopChunk);
 
     if (state.tokenCount % fieldCount !== 0) {
         return {
@@ -560,9 +562,9 @@ function result(data: Data.File) {
  *
  * @returns CifParserResult wrapper of the result.
  */
-async function parseInternal(data: string, ctx: Computation.Context) {
+async function parseInternal(data: string, runtimeCtx: RuntimeContext) {
     const dataBlocks: Data.Block[] = [];
-    const tokenizer = createTokenizer(data, ctx);
+    const tokenizer = createTokenizer(data, runtimeCtx);
     let blockHeader = '';
 
     let blockCtx = FrameContext();
@@ -574,7 +576,7 @@ async function parseInternal(data: string, ctx: Computation.Context) {
     let saveCtx = FrameContext();
     let saveFrame: Data.Frame = Data.SafeFrame(saveCtx.categoryNames, saveCtx.categories, '');
 
-    ctx.update({ message: 'Parsing...', current: 0, max: data.length });
+    runtimeCtx.update({ message: 'Parsing...', current: 0, max: data.length });
 
     moveNext(tokenizer);
     while (tokenizer.tokenType !== CifTokenType.End) {
@@ -641,7 +643,7 @@ async function parseInternal(data: string, ctx: Computation.Context) {
 }
 
 export default function parse(data: string) {
-    return Computation.create<Result<Data.File>>(async ctx => {
+    return Task.create<Result<Data.File>>('Parse CIF', async ctx => {
         return await parseInternal(data, ctx);
     });
 }
\ No newline at end of file
diff --git a/src/mol-io/reader/common/text/tokenizer.ts b/src/mol-io/reader/common/text/tokenizer.ts
index 15a8492c74ffd939be708e62e4858a8129291106..60f14c1b135bfca7d215f3a3d8939997b4ec2ae5 100644
--- a/src/mol-io/reader/common/text/tokenizer.ts
+++ b/src/mol-io/reader/common/text/tokenizer.ts
@@ -6,7 +6,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Computation from 'mol-util/computation'
+import { chunkedSubtask, RuntimeContext } from 'mol-task'
 
 export interface Tokenizer {
     data: string,
@@ -109,17 +109,17 @@ export namespace Tokenizer {
     }
 
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
-    export async function readLinesAsync(state: Tokenizer, count: number, chunker: Computation.Chunker): Promise<Tokens> {
+    export async function readLinesAsync(state: Tokenizer, count: number, ctx: RuntimeContext, initialLineCount = 100000): Promise<Tokens> {
         const { length } = state;
         const lineTokens = TokenBuilder.create(state, count * 2);
 
         let linesAlreadyRead = 0;
-        await chunker.process(chunkSize => {
+        await chunkedSubtask(ctx, initialLineCount, state, (chunkSize, state) => {
             const linesToRead = Math.min(count - linesAlreadyRead, chunkSize);
             readLinesChunk(state, linesToRead, lineTokens);
             linesAlreadyRead += linesToRead;
             return linesToRead;
-        }, update => update({ message: 'Parsing...', current: state.position, max: length }));
+        }, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: length }));
 
         return lineTokens;
     }
diff --git a/src/mol-io/reader/csv/parser.ts b/src/mol-io/reader/csv/parser.ts
index 3d80e906e9c57eb219614cc171a243a0949a746a..66a7a9e1783c3107a9b618cbb0fbbaf0fdd4a6e2 100644
--- a/src/mol-io/reader/csv/parser.ts
+++ b/src/mol-io/reader/csv/parser.ts
@@ -9,7 +9,7 @@ import { Tokens, TokenBuilder, Tokenizer } from '../common/text/tokenizer'
 import * as Data from './data-model'
 import Field from './field'
 import Result from '../result'
-import Computation from 'mol-util/computation'
+import { Task, RuntimeContext, chunkedSubtask, } from 'mol-task'
 
 const enum CsvTokenType {
     Value = 0,
@@ -22,7 +22,7 @@ interface State {
     tokenizer: Tokenizer,
 
     tokenType: CsvTokenType;
-    chunker: Computation.Chunker,
+    runtimeCtx: RuntimeContext,
     tokens: Tokens[],
 
     fieldCount: number,
@@ -38,7 +38,7 @@ interface State {
     noColumnNamesRecord: boolean
 }
 
-function State(data: string, ctx: Computation.Context, opts: CsvOptions): State {
+function State(data: string, runtimeCtx: RuntimeContext, opts: CsvOptions): State {
 
     const tokenizer = Tokenizer(data)
     return {
@@ -46,7 +46,7 @@ function State(data: string, ctx: Computation.Context, opts: CsvOptions): State
         tokenizer,
 
         tokenType: CsvTokenType.End,
-        chunker: Computation.chunker(ctx, 100000),
+        runtimeCtx,
         tokens: [],
 
         fieldCount: 0,
@@ -206,7 +206,7 @@ function moveNext(state: State) {
     return newRecord
 }
 
-function readRecordsChunk(state: State, chunkSize: number) {
+function readRecordsChunk(chunkSize: number, state: State) {
     if (state.tokenType === CsvTokenType.End) return 0
 
     let newRecord = moveNext(state);
@@ -225,9 +225,8 @@ function readRecordsChunk(state: State, chunkSize: number) {
 }
 
 function readRecordsChunks(state: State) {
-    return state.chunker.process(
-        chunkSize => readRecordsChunk(state, chunkSize),
-        update => update({ message: 'Parsing...', current: state.tokenizer.position, max: state.data.length }));
+    return chunkedSubtask(state.runtimeCtx, 100000, state, readRecordsChunk,
+        (ctx, state) => ctx.update({ message: 'Parsing...', current: state.tokenizer.position, max: state.data.length }));
 }
 
 function addColumn (state: State) {
@@ -261,7 +260,7 @@ async function handleRecords(state: State): Promise<Data.Table> {
     return Data.Table(state.recordCount, state.columnNames, columns)
 }
 
-async function parseInternal(data: string, ctx: Computation.Context, opts: CsvOptions): Promise<Result<Data.File>> {
+async function parseInternal(data: string, ctx: RuntimeContext, opts: CsvOptions): Promise<Result<Data.File>> {
     const state = State(data, ctx, opts);
 
     ctx.update({ message: 'Parsing...', current: 0, max: data.length });
@@ -279,7 +278,7 @@ interface CsvOptions {
 
 export function parse(data: string, opts?: Partial<CsvOptions>) {
     const completeOpts = Object.assign({}, { quote: '"', comment: '#', delimiter: ',', noColumnNames: false }, opts)
-    return Computation.create<Result<Data.File>>(async ctx => {
+    return Task.create<Result<Data.File>>('Parse CSV', async ctx => {
         return await parseInternal(data, ctx, completeOpts);
     });
 }
diff --git a/src/mol-io/reader/gro/parser.ts b/src/mol-io/reader/gro/parser.ts
index d1c71ec315c3f98d47f45692838c9dfa462d4142..eee90135b4a588a565b25ee1f5354f150d987f3b 100644
--- a/src/mol-io/reader/gro/parser.ts
+++ b/src/mol-io/reader/gro/parser.ts
@@ -10,13 +10,13 @@ import Tokenizer from '../common/text/tokenizer'
 import FixedColumn from '../common/text/column/fixed'
 import * as Schema from './schema'
 import Result from '../result'
-import Computation from 'mol-util/computation'
+import { Task, RuntimeContext } from 'mol-task'
 
 interface State {
     tokenizer: Tokenizer,
     header: Schema.Header,
     numberOfAtoms: number,
-    chunker: Computation.Chunker
+    runtimeCtx: RuntimeContext
 }
 
 function createEmptyHeader(): Schema.Header {
@@ -29,12 +29,12 @@ function createEmptyHeader(): Schema.Header {
     };
 }
 
-function State(tokenizer: Tokenizer, ctx: Computation.Context): State {
+function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext): State {
     return {
         tokenizer,
         header: createEmptyHeader(),
         numberOfAtoms: 0,
-        chunker: Computation.chunker(ctx, 100000) // 100000 lines is the default chunk size for this reader
+        runtimeCtx
     };
 }
 
@@ -90,7 +90,7 @@ function handleNumberOfAtoms(state: State) {
  */
 async function handleAtoms(state: State): Promise<Schema.Atoms> {
     const { tokenizer, numberOfAtoms } = state;
-    const lines = await Tokenizer.readLinesAsync(tokenizer, numberOfAtoms, state.chunker);
+    const lines = await Tokenizer.readLinesAsync(tokenizer, numberOfAtoms, state.runtimeCtx, 100000);
 
     const positionSample = tokenizer.data.substring(lines.indices[0], lines.indices[1]).substring(20);
     const precisions = positionSample.match(/\.\d+/g)!;
@@ -137,7 +137,7 @@ function handleBoxVectors(state: State) {
     state.header.box = [+values[0], +values[1], +values[2]];
 }
 
-async function parseInternal(data: string, ctx: Computation.Context): Promise<Result<Schema.File>> {
+async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.File>> {
     const tokenizer = Tokenizer(data);
 
     ctx.update({ message: 'Parsing...', current: 0, max: data.length });
@@ -156,7 +156,7 @@ async function parseInternal(data: string, ctx: Computation.Context): Promise<Re
 }
 
 export function parse(data: string) {
-    return Computation.create<Result<Schema.File>>(async ctx => {
+    return Task.create<Result<Schema.File>>('Parse GRO', async ctx => {
         return await parseInternal(data, ctx);
     });
 }
diff --git a/src/mol-io/reader/mol2/parser.ts b/src/mol-io/reader/mol2/parser.ts
index f807f4ff7b78d1c3f7a051fbba88a0a69c6216de..32126acb9050dbf33e490f2d9ffed2ca98a5f76c 100644
--- a/src/mol-io/reader/mol2/parser.ts
+++ b/src/mol-io/reader/mol2/parser.ts
@@ -6,7 +6,7 @@
  */
 
 //               NOTES
-//When want to created undefined string column, must use
+// When want to created undefined string column, must use
 // undefStr = UndefinedColumn(molecule.num_atoms, ColumnType.str)
 // but not
 // const undefPooledStr = UndefinedColumn(molecule.num_atoms, ColumnType.pooledStr);
@@ -16,14 +16,14 @@ import { TokenBuilder, Tokenizer } from '../common/text/tokenizer'
 import TokenColumn from '../common/text/column/token'
 import * as Schema from './schema'
 import Result from '../result'
-import Computation from 'mol-util/computation'
+import { Task, RuntimeContext, chunkedSubtask } from 'mol-task'
 
 const { skipWhitespace, eatValue, markLine, getTokenString, readLine } = Tokenizer;
 
 interface State {
     tokenizer: Tokenizer,
     molecule: Schema.Molecule,
-    chunker: Computation.Chunker
+    runtimeCtx: RuntimeContext
 }
 
 function createEmptyMolecule(): Schema.Molecule {
@@ -36,16 +36,16 @@ function createEmptyMolecule(): Schema.Molecule {
         num_sets: 0,
         mol_type: '',
         charge_type: '',
-        status_bits:'',
+        status_bits: '',
         mol_comment: ''
     };
 }
 
-function State(tokenizer: Tokenizer, ctx: Computation.Context): State {
+function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext): State {
     return {
         tokenizer,
         molecule: createEmptyMolecule(),
-        chunker: Computation.chunker(ctx, 100000)
+        runtimeCtx
     };
 }
 
@@ -54,7 +54,7 @@ const reWhitespace = /\s+/g;
 function handleMolecule(state: State) {
     const { tokenizer, molecule } = state;
 
-    while(getTokenString(tokenizer) !== '@<TRIPOS>MOLECULE'){
+    while (getTokenString(tokenizer) !== '@<TRIPOS>MOLECULE') {
         markLine(tokenizer);
     }
 
@@ -84,10 +84,10 @@ function handleMolecule(state: State) {
     molecule.mol_comment = getTokenString(tokenizer)
 }
 
-function isStatus_bit(aString: String): Boolean{
-    if(aString.includes('DSPMOD') || aString.includes('TYPECOL') || aString.includes('CAP')
-       || aString.includes('BACKBONE') || aString.includes('DICT') || aString.includes('ESSENTIAL')
-       || aString.includes('WATER') || aString.includes('DIRECT')){
+function isStatus_bit(aString: String): Boolean {
+    if (aString.includes('DSPMOD') || aString.includes('TYPECOL') || aString.includes('CAP')
+        || aString.includes('BACKBONE') || aString.includes('DICT') || aString.includes('ESSENTIAL')
+        || aString.includes('WATER') || aString.includes('DIRECT')) {
         return true;
     }
     return false;
@@ -101,7 +101,7 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> {
     let hasStatus_bit = false;
 
     // skip empty lines and '@<TRIPOS>ATOM'
-    while(getTokenString(tokenizer) != '@<TRIPOS>ATOM'){
+    while (getTokenString(tokenizer) !== '@<TRIPOS>ATOM') {
         markLine(tokenizer);
     }
 
@@ -113,17 +113,17 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> {
 
     // optional columns are in order "integer string float string".
     // Use this to find out which column is missing or empty
-    for(let i = 6; i < firstLineLength; i++){
-        if(!isNaN(Number(firstLineArray[i]))){
-            if(firstLineArray[i].indexOf('.') == -1){
+    for (let i = 6; i < firstLineLength; i++) {
+        if (!isNaN(Number(firstLineArray[i]))) {
+            if (firstLineArray[i].indexOf('.') === -1) {
                 hasSubst_id = true;
-            }else{
+            } else {
                 hasCharge = true;
             }
-        }else if(isNaN(Number(firstLineArray[i]))){
-            if(!isStatus_bit(firstLineArray[i])){
+        } else if (isNaN(Number(firstLineArray[i]))) {
+            if (!isStatus_bit(firstLineArray[i])) {
                 hasSubst_name = true;
-            }else{
+            } else {
                 hasStatus_bit = true;
             }
         }
@@ -131,7 +131,7 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> {
 
     // required columns
     const atom_idTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const atom_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);;
+    const atom_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
     const xTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
     const yTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
     const zTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
@@ -160,28 +160,28 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> {
     const undefStr = Column.Undefined(molecule.num_atoms, Column.Schema.str);
 
     let numOfColumn = 6;
-    if(hasSubst_id){numOfColumn++}
-    if(hasSubst_name){numOfColumn++}
-    if(hasCharge){numOfColumn++}
-    if(hasStatus_bit){numOfColumn++}
+    if (hasSubst_id) { numOfColumn++ }
+    if (hasSubst_name) { numOfColumn++ }
+    if (hasCharge) { numOfColumn++ }
+    if (hasStatus_bit) { numOfColumn++ }
 
     tokenizer.position = initialTokenizerPosition;
     tokenizer.lineNumber = initialTokenizerLineNumber;
 
     const { length } = tokenizer;
     let linesAlreadyRead = 0;
-    await state.chunker.process(chunkSize => {
+    await chunkedSubtask(state.runtimeCtx, 100000, void 0, chunkSize => {
         const linesToRead = Math.min(molecule.num_atoms - linesAlreadyRead, chunkSize);
-        for(let i = 0; i < linesToRead; i++){
+        for (let i = 0; i < linesToRead; i++) {
             let subst_idWritten = false;
             let subst_nameWritten = false;
             let chargeWritten = false;
             let status_bitWritten = false;
-            for(let j = 0; j < numOfColumn; j++){
+            for (let j = 0; j < numOfColumn; j++) {
                 skipWhitespace(tokenizer);
                 tokenizer.tokenStart = tokenizer.position;
                 eatValue(tokenizer);
-                switch(j){
+                switch (j) {
                     case 0:
                         TokenBuilder.addUnchecked(atom_idTokens, tokenizer.tokenStart, tokenizer.tokenEnd);
                         break;
@@ -201,16 +201,16 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> {
                         TokenBuilder.addUnchecked(atom_typeTokens, tokenizer.tokenStart, tokenizer.tokenEnd);
                         break;
                     default:
-                        if(hasSubst_id === true && subst_idWritten === false){
+                        if (hasSubst_id === true && subst_idWritten === false) {
                             TokenBuilder.addUnchecked(subst_idTokens, tokenizer.tokenStart, tokenizer.tokenEnd);
                             subst_idWritten = true;
-                        }else if(hasSubst_name === true && subst_nameWritten === false){
+                        } else if (hasSubst_name === true && subst_nameWritten === false) {
                             TokenBuilder.addUnchecked(subst_nameTokens, tokenizer.tokenStart, tokenizer.tokenEnd);
                             subst_nameWritten = true;
-                        }else if(hasCharge === true && chargeWritten === false){
+                        } else if (hasCharge === true && chargeWritten === false) {
                             TokenBuilder.addUnchecked(chargeTokens, tokenizer.tokenStart, tokenizer.tokenEnd);
                             chargeWritten = true;
-                        }else if(hasStatus_bit === true && status_bitWritten === false){
+                        } else if (hasStatus_bit === true && status_bitWritten === false) {
                             TokenBuilder.addUnchecked(status_bitTokens, tokenizer.tokenStart, tokenizer.tokenEnd);
                             status_bitWritten = true;
                         }
@@ -219,7 +219,7 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> {
         }
         linesAlreadyRead += linesToRead;
         return linesToRead;
-    }, update => update({ message: 'Parsing...', current: tokenizer.position, max: length }));
+    }, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length }));
 
     const ret = {
         count: molecule.num_atoms,
@@ -243,7 +243,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> {
     const { tokenizer, molecule } = state;
     let hasStatus_bit = false;
 
-    while(getTokenString(tokenizer) !== '@<TRIPOS>BOND'){
+    while (getTokenString(tokenizer) !== '@<TRIPOS>BOND') {
         markLine(tokenizer);
     }
 
@@ -252,7 +252,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> {
     const firstLine = readLine(tokenizer);
     const firstLineArray = firstLine.trim().split(/\s+/g)
     const firstLineLength = firstLineArray.length;
-    if(firstLineLength === 5){
+    if (firstLineLength === 5) {
         hasStatus_bit = true;
     }
 
@@ -273,21 +273,21 @@ async function handleBonds(state: State): Promise<Schema.Bonds> {
     const undefStr = Column.Undefined(molecule.num_bonds, Column.Schema.str);
 
     let numberOfColumn = 4;
-    if(hasStatus_bit){numberOfColumn++}
+    if (hasStatus_bit) { numberOfColumn++ }
 
     tokenizer.position = initialTokenizerPosition;
     tokenizer.lineNumber = initialTokenizerLineNumber;
 
     const { length } = tokenizer;
     let linesAlreadyRead = 0;
-    await state.chunker.process(chunkSize => {
+    await chunkedSubtask(state.runtimeCtx, 100000, void 0, chunkSize => {
         const linesToRead = Math.min(molecule.num_bonds - linesAlreadyRead, chunkSize);
-        for(let i = 0; i < linesToRead; i++){
-            for(let j = 0; j < numberOfColumn; j++){
+        for (let i = 0; i < linesToRead; i++) {
+            for (let j = 0; j < numberOfColumn; j++) {
                 skipWhitespace(tokenizer);
                 tokenizer.tokenStart = tokenizer.position;
                 eatValue(tokenizer);
-                switch(j){
+                switch (j) {
                     case 0:
                         TokenBuilder.addUnchecked(bond_idTokens, tokenizer.tokenStart, tokenizer.tokenEnd);
                         break;
@@ -308,7 +308,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> {
         }
         linesAlreadyRead += linesToRead;
         return linesToRead;
-    }, update => update({ message: 'Parsing...', current: tokenizer.position, max: length }));
+    }, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length }));
 
     const ret = {
         count: molecule.num_bonds,
@@ -324,7 +324,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> {
     return ret;
 }
 
-async function parseInternal(data: string, ctx: Computation.Context): Promise<Result<Schema.File>> {
+async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.File>> {
     const tokenizer = Tokenizer(data);
 
     ctx.update({ message: 'Parsing...', current: 0, max: data.length });
@@ -342,7 +342,7 @@ async function parseInternal(data: string, ctx: Computation.Context): Promise<Re
 }
 
 export function parse(data: string) {
-    return Computation.create<Result<Schema.File>>(async ctx => {
+    return Task.create<Result<Schema.File>>('Parse MOL2', async ctx => {
         return await parseInternal(data, ctx);
     });
 }
diff --git a/src/mol-script/compiler.ts b/src/mol-script/compiler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..758322a6341b800bf710e47d6df3bf3011084a57
--- /dev/null
+++ b/src/mol-script/compiler.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Expression from './expression'
+import Environment from './runtime/environment'
+import RuntimeExpression from './runtime/expression'
+import SymbolRuntime, { RuntimeArguments } from './runtime/symbol'
+
+export type CompiledExpression<C, T> = (ctx: C) => T
+
+type Compiler<C> = <T>(expression: Expression) => CompiledExpression<C, T>
+function Compiler<C>(envProvider: (ctx?: C) => Environment): Compiler<C> {
+    const env = envProvider(void 0);
+    return expression => wrap(envProvider, compile(env, expression).runtime);
+}
+
+type CompileResult = { isConst: boolean, runtime: RuntimeExpression }
+
+namespace CompileResult {
+    export function Const(value: any): CompileResult { return { isConst: true, runtime: RuntimeExpression.constant(value) } }
+    export function Dynamic(runtime: RuntimeExpression): CompileResult { return { isConst: false, runtime } }
+}
+
+function wrap<C, T>(envProvider: (ctx?: C) => Environment, runtime: RuntimeExpression<C, T>) {
+    return (ctx: C) => runtime(envProvider(ctx));
+}
+
+function noRuntimeFor(symbol: string) {
+    throw new Error(`Could not find runtime for symbol '${symbol}'.`);
+}
+
+function applySymbolStatic(runtime: SymbolRuntime, args: RuntimeArguments) {
+    return CompileResult.Dynamic(env => runtime(env, args))
+}
+
+function applySymbolDynamic(head: RuntimeExpression, args: RuntimeArguments) {
+    return CompileResult.Dynamic(env => {
+        const value = head(env);
+        const symbol = env.symbolTable[value];
+        if (!symbol) noRuntimeFor(value);
+        return symbol.runtime(env, args);
+    })
+}
+
+function apply(env: Environment, head: CompileResult, args: RuntimeArguments, constArgs: boolean): CompileResult {
+    if (head.isConst) {
+        const value = head.runtime(env);
+        const symbol = env.symbolTable[value];
+        if (!symbol) throw new Error(`Could not find runtime for symbol '${value}'.`);
+        if (symbol.attributes.isStatic && constArgs) return CompileResult.Const(symbol.runtime(env, args));
+        return applySymbolStatic(symbol.runtime, args);
+    }
+    return applySymbolDynamic(head.runtime, args);
+}
+
+function compile(env: Environment, expression: Expression): CompileResult {
+    if (Expression.isLiteral(expression)) {
+        return CompileResult.Const(expression);
+    }
+
+    const head = compile(env, expression.head);
+    if (!expression.args) {
+        return apply(env, head, [], true);
+    } else if (Expression.isArgumentsArray(expression.args)) {
+        const args = [];
+        let constArgs = false;
+        for (const arg of expression.args) {
+            const compiled = compile(env, arg);
+            constArgs = constArgs && compiled.isConst;
+            args.push(compiled.runtime);
+        }
+        return apply(env, head, args as any, constArgs);
+    } else {
+        const args = Object.create(null);
+        let constArgs = false;
+        for (const key of Object.keys(expression.args)) {
+            const compiled = compile(env, expression.args[key]);
+            constArgs = constArgs && compiled.isConst;
+            args[key] = compiled.runtime;
+        }
+        return apply(env, head, args, constArgs);
+    }
+}
+
+export default Compiler
\ No newline at end of file
diff --git a/src/mol-script/container.ts b/src/mol-script/container.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f03655aa965ff19c85da5e3c4ffb3641b8e26e7b
--- /dev/null
+++ b/src/mol-script/container.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Expression from './expression'
+
+interface Container {
+    source?: string,
+    version: string,
+    expression: Expression
+}
+
+export default Container
\ No newline at end of file
diff --git a/src/mol-script/core-symbols.ts b/src/mol-script/core-symbols.ts
new file mode 100644
index 0000000000000000000000000000000000000000..67dd55ca4c4c077738192a299eefefa5aab20b79
--- /dev/null
+++ b/src/mol-script/core-symbols.ts
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Type from './type'
+import Symbol, { Arguments, Argument } from './symbol'
+import { symbol, normalizeTable, symbolList } from './helpers'
+
+export namespace Types {
+    export type List<T = any> = ArrayLike<T>
+    export type Set<T = any> = { has(e: T): boolean }
+
+    export const AnyVar = Type.Variable('a', Type.Any);
+    export const AnyValueVar = Type.Variable('a', Type.Any);
+    export const ConstrainedVar = Type.Variable('a', Type.Any, true);
+
+    export const Regex = Type.Value<RegExp>('Core', 'Regex');
+
+    export const Set = <T extends Type>(t?: T) => Type.Container<Set<T['@type']>>('Core', 'Set', t || AnyValueVar);
+    export const List = <T extends Type>(t?: T) => Type.Container<List<T['@type']>>('Core', 'List', t || AnyVar);
+    export const Fn = <T extends Type>(t?: T, alias?: string) => Type.Container<(env: any) => T['@type']>('Core', 'Fn', t || AnyVar, alias);
+    export const Flags = <T extends Type>(t: T, alias?: string) => Type.Container<number>('Core', 'Flags', t, alias);
+
+    export const BitFlags = Flags(Type.Num, 'BitFlags');
+}
+
+function unaryOp<T extends Type>(type: T, description?: string) {
+    return symbol(Arguments.Dictionary({ 0: Argument(type) }), type, description);
+}
+
+function binOp<T extends Type>(type: T, description?: string) {
+    return symbol(Arguments.List(type, { nonEmpty: true }), type, description);
+}
+
+function binRel<A extends Type, T extends Type>(src: A, target: T, description?: string) {
+    return symbol(Arguments.Dictionary({
+        0: Argument(src),
+        1: Argument(src)
+    }), target, description);
+}
+
+const type = {
+    '@header': 'Types',
+    bool: symbol(Arguments.Dictionary({ 0: Argument(Type.AnyValue) }), Type.Bool, 'Convert a value to boolean.'),
+    num: symbol(Arguments.Dictionary({ 0: Argument(Type.AnyValue) }), Type.Num, 'Convert a value to number.'),
+    str: symbol(Arguments.Dictionary({ 0: Argument(Type.AnyValue) }), Type.Str, 'Convert a value to string.'),
+    regex: symbol(
+        Arguments.Dictionary({
+            0: Argument(Type.Str, { description: 'Expression' }),
+            1: Argument(Type.Str, { isOptional: true, description: `Flags, e.g. 'i' for ignore case` })
+        }), Types.Regex, 'Creates a regular expression from a string using the ECMAscript syntax.'),
+
+    list: symbol(Arguments.List(Types.AnyVar), Types.List()),
+    set: symbol(Arguments.List(Types.AnyValueVar), Types.Set()),
+    bitflags: symbol(Arguments.Dictionary({ 0: Argument(Type.Num) }), Types.BitFlags, 'Interpret a number as bitflags.'),
+    compositeKey: symbol(Arguments.List(Type.AnyValue), Type.AnyValue),
+};
+
+const logic = {
+    '@header': 'Logic',
+    not: unaryOp(Type.Bool),
+    and: binOp(Type.Bool),
+    or: binOp(Type.Bool),
+};
+
+const ctrl = {
+    '@header': 'Control',
+    eval: symbol(Arguments.Dictionary({ 0: Argument(Types.Fn(Types.AnyVar)) }), Types.AnyVar, 'Evaluate a function.'),
+    fn: symbol(Arguments.Dictionary({ 0: Argument(Types.AnyVar) }), Types.Fn(Types.AnyVar), 'Wrap an expression to a "lazy" function.'),
+    if: symbol(Arguments.Dictionary({
+        0: Argument(Type.Bool, { description: 'Condition' }),
+        1: Argument(Type.Variable('a', Type.Any), { description: 'If true' }),
+        2: Argument(Type.Variable('b', Type.Any), { description: 'If false' })
+    }), Type.Union([Type.Variable('a', Type.Any), Type.Variable('b', Type.Any)])),
+    assoc: symbol(Arguments.Dictionary({
+        0: Argument(Type.Str, { description: 'Name' }),
+        1: Argument(Type.Variable('a', Type.Any), {description: 'Value to assign' })
+    }), Type.Variable('a', Type.Any))
+};
+
+const rel = {
+    '@header': 'Relational',
+    eq: binRel(Type.Variable('a', Type.AnyValue, true), Type.Bool),
+    neq: binRel(Type.Variable('a', Type.AnyValue, true), Type.Bool),
+    lt: binRel(Type.Num, Type.Bool),
+    lte: binRel(Type.Num, Type.Bool),
+    gr: binRel(Type.Num, Type.Bool),
+    gre: binRel(Type.Num, Type.Bool),
+    inRange: symbol(Arguments.Dictionary({
+        0: Argument(Type.Num, { description: 'Value to test' }),
+        1: Argument(Type.Num, { description: 'Minimum value' }),
+        2: Argument(Type.Num, { description: 'Maximum value' })
+    }), Type.Bool, 'Check if the value of the 1st argument is >= 2nd and <= 3rd.'),
+};
+
+const math = {
+    '@header': 'Math',
+    add: binOp(Type.Num),
+    sub: binOp(Type.Num),
+    mult: binOp(Type.Num),
+    div: binRel(Type.Num, Type.Num),
+    pow: binRel(Type.Num, Type.Num),
+    mod: binRel(Type.Num, Type.Num),
+
+    min: binOp(Type.Num),
+    max: binOp(Type.Num),
+
+    floor: unaryOp(Type.Num),
+    ceil: unaryOp(Type.Num),
+    roundInt: unaryOp(Type.Num),
+    abs: unaryOp(Type.Num),
+    sqrt: unaryOp(Type.Num),
+    sin: unaryOp(Type.Num),
+    cos: unaryOp(Type.Num),
+    tan: unaryOp(Type.Num),
+    asin: unaryOp(Type.Num),
+    acos: unaryOp(Type.Num),
+    atan: unaryOp(Type.Num),
+    sinh: unaryOp(Type.Num),
+    cosh: unaryOp(Type.Num),
+    tanh: unaryOp(Type.Num),
+    exp: unaryOp(Type.Num),
+    log: unaryOp(Type.Num),
+    log10: unaryOp(Type.Num),
+    atan2: binRel(Type.Num, Type.Num)
+};
+
+const str = {
+    '@header': 'Strings',
+    concat: binOp(Type.Str),
+    match: symbol(Arguments.Dictionary({ 0: Argument(Types.Regex), 1: Argument(Type.Str) }), Type.Bool)
+};
+
+const list = {
+    '@header': 'Lists',
+    getAt: symbol(Arguments.Dictionary({ 0: Argument(Types.List()), 1: Argument(Type.Num) }), Types.AnyVar)
+};
+
+const set = {
+    '@header': 'Sets',
+    has: symbol(Arguments.Dictionary({ 0: Argument(Types.Set(Types.ConstrainedVar)), 1: Argument(Types.ConstrainedVar) }), Type.Bool, 'Check if the the 1st argument includes the value of the 2nd.'),
+    isSubset: symbol(Arguments.Dictionary({ 0: Argument(Types.Set(Types.ConstrainedVar)), 1: Argument(Types.Set(Types.ConstrainedVar)) }), Type.Bool, 'Check if the the 1st argument is a subset of the 2nd.')
+};
+
+const flags = {
+    '@header': 'Flags',
+    hasAny: symbol(Arguments.Dictionary({
+        0: Argument(Types.Flags(Types.ConstrainedVar)),
+        1: Argument(Types.Flags(Types.ConstrainedVar))
+    }), Type.Bool, 'Check if the the 1st argument has at least one of the 2nd one\'s flags.'),
+    hasAll: symbol(Arguments.Dictionary({
+        0: Argument(Types.Flags(Types.ConstrainedVar)),
+        1: Argument(Types.Flags(Types.ConstrainedVar))
+    }), Type.Bool, 'Check if the the 1st argument has all 2nd one\'s flags.'),
+}
+
+const table = {
+    '@header': 'Language Primitives',
+    type,
+    logic,
+    ctrl,
+    rel,
+    math,
+    str,
+    list,
+    set,
+    flags
+}
+
+normalizeTable(table);
+
+export const SymbolList = symbolList(table);
+
+export const SymbolMap = (function() {
+    const map: { [id: string]: Symbol | undefined } = Object.create(null);
+    for (const s of SymbolList) map[s.id] = s;
+    return map;
+})();
+
+export default table;
+
+
diff --git a/src/mol-script/expression-formatter.ts b/src/mol-script/expression-formatter.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71e5ca6bfebdc638ff0210f0ab8d33ddf275826e
--- /dev/null
+++ b/src/mol-script/expression-formatter.ts
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Expression from './expression'
+
+const { isLiteral, isArgumentsArray } = Expression;
+
+export default function format(e: Expression) {
+    const writer = new Writer();
+    _format(e, writer);
+    return writer.getStr();
+}
+
+class Writer {
+    private value: string[] = [];
+    private currentLineLength = 0;
+    private prefixLength = 0;
+    private _prefix: string = '';
+    private localPrefix: string = '';
+
+    private setLocal() {
+        this.localPrefix = '  ';
+    }
+
+    newline() {
+        this.value.push(`\n${this._prefix}${this.localPrefix}`);
+        this.currentLineLength = 0;
+    }
+
+    push() {
+        this.value.push('(');
+        this.currentLineLength = 0;
+        this.localPrefix = '';
+        this.prefixLength += 2;
+        this._prefix = new Array(this.prefixLength + 1).join(' ');
+    }
+
+    pop() {
+        this.value.push(')');
+        this.prefixLength -= 2;
+        this._prefix = new Array(this.prefixLength + 1).join(' ');
+    }
+
+    append(str: string) {
+        if (!this.currentLineLength) {
+            this.value.push(str);
+            this.currentLineLength = str.length;
+        } else if (this.currentLineLength + this.prefixLength + this.localPrefix.length + str.length < 80) {
+            this.value.push(str);
+            this.currentLineLength += str.length;
+        } else {
+            this.setLocal();
+            this.newline();
+            this.value.push(str);
+            this.currentLineLength = str.length;
+        }
+    }
+
+    whitespace() {
+        if (this.currentLineLength + this.prefixLength + this.localPrefix.length + 1 < 80) {
+            this.value.push(' ');
+        }
+    }
+
+    getStr() {
+        return this.value.join('');
+    }
+}
+
+function _format(e: Expression, writer: Writer) {
+    if (isLiteral(e)) {
+        if (typeof e === 'string' && (/\s/.test(e) || !e.length)) writer.append(`\`${e}\``);
+        else writer.append(`${e}`);
+        return;
+    }
+
+    writer.push();
+    _format(e.head, writer);
+
+    if (!e.args) {
+        writer.pop();
+        return;
+    }
+
+    if (isArgumentsArray(e.args)) {
+        let prevLiteral = true;
+        for (const a of e.args) {
+            if (isLiteral(a)) {
+                if (prevLiteral) writer.whitespace();
+                else writer.newline();
+                prevLiteral = true;
+            } else {
+                prevLiteral = false;
+                writer.newline();
+            }
+            _format(a, writer);
+        }
+        writer.pop();
+        return;
+    }
+
+    const keys = Object.keys(e.args);
+    if (!keys.length) {
+        writer.pop();
+        return;
+    }
+
+    if (keys.length === 1 && isLiteral(e.args[keys[0]])) {
+        writer.whitespace()
+        writer.append(`:${keys[0]}`);
+        writer.whitespace();
+        _format(e.args[keys[0]], writer);
+        writer.pop();
+        return;
+    }
+
+    for (const a of keys) {
+        writer.newline();
+        writer.append(`:${a}`);
+        writer.whitespace();
+        _format(e.args[a], writer);
+    }
+    writer.pop();
+}
\ No newline at end of file
diff --git a/src/mol-script/expression.ts b/src/mol-script/expression.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c9ffdcb5b79b770be5b142be2a5a1adac3aeaaa2
--- /dev/null
+++ b/src/mol-script/expression.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+type Expression =
+    | Expression.Literal
+    | Expression.Apply
+
+namespace Expression {
+    export type Literal = string | number | boolean
+    export type Arguments = Expression[] | { [name: string]: Expression }
+    export interface Apply { readonly head: Expression, readonly args?: Arguments }
+
+    export function Apply(head: Expression, args?: Arguments): Apply { return args ? { head, args } : { head }; }
+
+    export function isArgumentsArray(e: Arguments): e is Expression[] { return e instanceof Array; }
+    export function isArgumentsMap(e: Arguments): e is { [name: string]: Expression } { return !(e instanceof Array); }
+    export function isLiteral(e: Expression): e is Expression.Literal { return !isApply(e); }
+    export function isApply(e: Expression): e is Expression.Apply { return !! e && !!(e as Expression.Apply).head && typeof e === 'object'; }
+}
+
+export default Expression
\ No newline at end of file
diff --git a/src/mol-script/helpers.ts b/src/mol-script/helpers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..09ced0a60a313962856c2f911a47ab3d645f4486
--- /dev/null
+++ b/src/mol-script/helpers.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Type from './type'
+import Symbol, { Arguments, isSymbol } from './symbol'
+
+export function symbol<A extends Arguments, T extends Type<S>, S>(args: A, type: T, description?: string) {
+    return Symbol('', args, type, description);
+}
+
+export function normalizeTable(table: any) {
+    _normalizeTable('', '', table);
+}
+
+export function symbolList(table: any): Symbol[] {
+    const list: Symbol[] = [];
+    _symbolList(table, list);
+    return list;
+}
+
+function formatKey(key: string) {
+    const regex = /([a-z])([A-Z])([a-z]|$)/g;
+    // do this twice because 'xXxX'
+    return key.replace(regex, (s, a, b, c) => `${a}-${b.toLocaleLowerCase()}${c}`).replace(regex, (s, a, b, c) => `${a}-${b.toLocaleLowerCase()}${c}`);
+}
+
+function _normalizeTable(namespace: string, key: string, obj: any) {
+    if (isSymbol(obj)) {
+        obj.info.namespace = namespace;
+        obj.info.name = obj.info.name || formatKey(key);
+        obj.id = `${obj.info.namespace}.${obj.info.name}`;
+        return;
+    }
+    const currentNs = `${obj['@namespace'] || formatKey(key)}`;
+    const newNs = namespace ? `${namespace}.${currentNs}` : currentNs;
+    for (const childKey of Object.keys(obj)) {
+        if (typeof obj[childKey] !== 'object' && !isSymbol(obj[childKey])) continue;
+        _normalizeTable(newNs, childKey, obj[childKey]);
+    }
+}
+
+function _symbolList(obj: any, list: Symbol[]) {
+    if (isSymbol(obj)) {
+        list.push(obj);
+        return;
+    }
+    for (const childKey of Object.keys(obj)) {
+        if (typeof obj[childKey] !== 'object' && !isSymbol(obj[childKey])) continue;
+        _symbolList(obj[childKey], list);
+    }
+}
\ No newline at end of file
diff --git a/src/mol-script/runtime/environment.ts b/src/mol-script/runtime/environment.ts
new file mode 100644
index 0000000000000000000000000000000000000000..24e7e8b3407dda4a879fccdbbc5f7d61d83a5d17
--- /dev/null
+++ b/src/mol-script/runtime/environment.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { SymbolRuntimeTable } from './symbol'
+
+interface Environment<T = any> {
+    readonly symbolTable: SymbolRuntimeTable,
+    readonly context: T
+}
+
+export default Environment
\ No newline at end of file
diff --git a/src/mol-script/runtime/expression.ts b/src/mol-script/runtime/expression.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c2e5a71c63e25a50955a8b5cd747452f67465d00
--- /dev/null
+++ b/src/mol-script/runtime/expression.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Environment from './environment'
+
+type RuntimeExpression<C = any, T = any> = (env: Environment<C>) => T
+
+export interface ExpressionInfo {
+    isConst?: boolean
+}
+
+namespace RuntimeExpression {
+    export function constant<C, T>(c: T): RuntimeExpression<C, T> {
+        return env => c;
+    }
+
+    export function func<C, T>(f: (env: Environment<C>) => T): RuntimeExpression<C, T> {
+        return env => f(env);
+    }
+}
+
+export default RuntimeExpression
\ No newline at end of file
diff --git a/src/mol-script/runtime/symbol.ts b/src/mol-script/runtime/symbol.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15b4493ad51ee1a7b98e66922ad740e411f02b56
--- /dev/null
+++ b/src/mol-script/runtime/symbol.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Environment from './environment'
+import RuntimeExpression from './expression'
+
+export type RuntimeArguments = ArrayLike<RuntimeExpression> | { [name: string]: RuntimeExpression | undefined }
+
+type SymbolRuntime = (env: Environment, args: RuntimeArguments) => any
+
+namespace SymbolRuntime {
+    export interface Info {
+        readonly runtime: SymbolRuntime,
+        readonly attributes: Attributes
+    }
+
+    export interface Attributes { isStatic: boolean }
+}
+
+function SymbolRuntime(symbol: Symbol, attributes: Partial<SymbolRuntime.Attributes> = {}) {
+    const { isStatic = false } = attributes;
+    return (runtime: SymbolRuntime): SymbolRuntime.Info => {
+        return ({ runtime, attributes: { isStatic } });
+    };
+}
+
+export type SymbolRuntimeTable = { readonly [id: string]: SymbolRuntime.Info }
+
+export default SymbolRuntime
\ No newline at end of file
diff --git a/src/mol-script/symbol.ts b/src/mol-script/symbol.ts
new file mode 100644
index 0000000000000000000000000000000000000000..34630be3fa2e936aef935d3ac68e19c024628c0b
--- /dev/null
+++ b/src/mol-script/symbol.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Type from './type'
+import Expression from './expression'
+
+export type Argument<T extends Type = Type>  = {
+    type: T,
+    isOptional: boolean,
+    isRest: boolean,
+    defaultValue: T['@type'] | undefined,
+    description: string | undefined
+}
+export function Argument<T extends Type>(type: T, params?: { description?: string, defaultValue?: T['@type'], isOptional?: boolean, isRest?: boolean }): Argument<T> {
+    const { description = void 0, isOptional = false, isRest = false, defaultValue = void 0 } = params || {}
+    return { type, isOptional, isRest, defaultValue, description };
+}
+
+export type Arguments<T extends { [key: string]: any } = {}> =
+    | Arguments.List<T>
+    | Arguments.Dictionary<T>
+
+export namespace Arguments {
+    export const None: Arguments = Dictionary({});
+
+    export interface Dictionary<T extends { [key: string]: any } = {}, Traits = {}> {
+        kind: 'dictionary',
+        map: { [P in keyof T]: Argument<T[P]> },
+        '@type': T
+    }
+    export function Dictionary<Map extends { [key: string]: Argument<any> }>(map: Map): Arguments<{ [P in keyof Map]: Map[P]['type']['@type'] }> {
+        return { kind: 'dictionary', map, '@type': 0 as any };
+    }
+
+    export interface List<T extends { [key: string]: any } = {}, Traits = {}> {
+        kind: 'list',
+        type: Type,
+        nonEmpty: boolean,
+        '@type': T
+    }
+
+    export function List<T extends Type>(type: T, params?: { nonEmpty?: boolean }): Arguments<{ [key: string]: T['@type'] }> {
+        const { nonEmpty = false } = params || { }
+        return { kind: 'list', type, nonEmpty, '@type': 0 as any };
+    }
+}
+
+export type ExpressionArguments<T> = { [P in keyof T]?: Expression } | { [index: number]: Expression }
+
+interface Symbol<A extends Arguments = Arguments, T extends Type = Type> {
+    (args?: ExpressionArguments<A['@type']>): Expression,
+    info: {
+        namespace: string,
+        name: string,
+        description?: string
+    },
+    args: A
+    type: T,
+    id: string,
+}
+
+function Symbol<A extends Arguments, T extends Type>(name: string, args: A, type: T, description?: string) {
+    const symbol: Symbol<A, T> = function(args: ExpressionArguments<A['@type']>) {
+        return Expression.Apply(symbol.id, args as any);
+    } as any;
+    symbol.info = { namespace: '', name, description };
+    symbol.id = '';
+    symbol.args = args;
+    symbol.type = type;
+    return symbol;
+}
+
+export function isSymbol(x: any): x is Symbol {
+    const s = x as Symbol;
+    return typeof s === 'function' && !!s.info && !!s.args && typeof s.info.namespace === 'string' && !!s.type;
+}
+
+export type SymbolMap = { [id: string]: Symbol | undefined }
+
+export default Symbol
+
diff --git a/src/mol-script/type.ts b/src/mol-script/type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e51a1237cffe2847eb05e0cd8eaaa4a280f07dc1
--- /dev/null
+++ b/src/mol-script/type.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+type Type<T = any> =
+    | Type.Any | Type.AnyValue | Type.Variable<T> | Type.Value<T>
+    | Type.Container<T> | Type.Union<T> | Type.OneOf<T>
+
+namespace Type {
+    export interface Any { kind: 'any',  '@type': any }
+    export interface Variable<T> { kind: 'variable', name: string, type: Type, isConstraint: boolean, '@type': any }
+    export interface AnyValue { kind: 'any-value', '@type': any }
+    export interface Value<T> { kind: 'value', namespace: string, name: string, parent?: Value<any>, '@type': T }
+    export interface Identifier { kind: 'identifier', '@type': string }
+    export interface Container<T> { kind: 'container', namespace: string, name: string, alias?: string, child: Type, '@type': T }
+    export interface Union<T> { kind: 'union', types: Type[], '@type': T }
+    export interface OneOf<T> { kind: 'oneof', type: Value<T>, namespace: string, name: string, values: { [v: string]: boolean | undefined }, '@type': T }
+
+    export function Variable<T = any>(name: string, type: Type, isConstraint?: boolean): Variable<T> { return { kind: 'variable', name, type: type, isConstraint } as any; }
+    export function Value<T>(namespace: string, name: string, parent?: Value<any>): Value<T> { return { kind: 'value', namespace, name, parent } as any; }
+    export function Container<T = any>(namespace: string, name: string, child: Type, alias?: string): Container<T> { return { kind: 'container', namespace, name, child, alias } as any; }
+    export function Union<T = any>(types: Type[]): Union<T> { return { kind: 'union', types } as any; }
+    export function OneOf<T = any>(namespace: string, name: string, type: Value<T>, values: any[]): OneOf<T> {
+        const vals = Object.create(null);
+        for (const v of values) vals[v] = true;
+        return { kind: 'oneof', namespace, name, type, values: vals } as any;
+    }
+
+    export const Any: Any = { kind: 'any' } as any;
+    export const AnyValue: AnyValue = { kind: 'any-value' } as any;
+
+    export const Num = Value<number>('', 'Number');
+    export const Str = Value<string>('', 'String');
+    export const Bool = OneOf<boolean>('', 'Bool', Str as any, ['true', 'false']);
+
+    export function oneOfValues({ values }: OneOf<any>) { return Object.keys(values).sort(); }
+}
+
+export default Type
\ No newline at end of file
diff --git a/src/mol-task/execution/observable.ts b/src/mol-task/execution/observable.ts
index 8c184c180944438d788b63a0000311d596a8c9f8..60d6c4e291d4e3c6fd5d37de1312604fb6652c1b 100644
--- a/src/mol-task/execution/observable.ts
+++ b/src/mol-task/execution/observable.ts
@@ -127,6 +127,8 @@ function notifyObserver(info: ProgressInfo, time: number) {
 }
 
 class ObservableRuntimeContext implements RuntimeContext {
+    isSynchronous = false;
+
     isExecuting = true;
     lastUpdatedTime = 0;
 
@@ -158,9 +160,9 @@ class ObservableRuntimeContext implements RuntimeContext {
         } else {
             if (typeof update.canAbort !== 'undefined') progress.canAbort = update.canAbort;
             if (typeof update.current !== 'undefined') progress.current = update.current;
-            if (typeof update.isIndeterminate !== 'undefined') progress.isIndeterminate = update.isIndeterminate;
             if (typeof update.max !== 'undefined') progress.max = update.max;
             if (typeof update.message !== 'undefined') progress.message = update.message;
+            progress.isIndeterminate = typeof progress.current === 'undefined' || typeof progress.max === 'undefined';
         }
     }
 
diff --git a/src/mol-task/execution/progress.ts b/src/mol-task/execution/progress.ts
index 513c262c67574879b6bf17f952f2569f00d8ebd1..4bf829dcb94c35de288a0fdf92b61a89213f8c0c 100644
--- a/src/mol-task/execution/progress.ts
+++ b/src/mol-task/execution/progress.ts
@@ -19,6 +19,21 @@ namespace Progress {
     }
 
     export interface Observer { (progress: Progress): void }
+
+    function _format(root: Progress.Node, prefix = ''): string {
+        const p = root.progress;
+        if (!root.children.length) {
+            if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}`;
+            return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}`;
+        }
+
+        const newPrefix = prefix + '  |_ ';
+        const subTree = root.children.map(c => _format(c, newPrefix));
+        if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}\n${subTree.join('\n')}`;
+        return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}\n${subTree.join('\n')}`;
+    }
+
+    export function format(p: Progress) { return _format(p.root); }
 }
 
 export { Progress }
\ No newline at end of file
diff --git a/src/mol-task/execution/runtime-context.ts b/src/mol-task/execution/runtime-context.ts
index e8812c55f733fbbd70779ec98bc5664f656024f8..d2529a3eb3599d9ece65c7183b7a36cdc0117b33 100644
--- a/src/mol-task/execution/runtime-context.ts
+++ b/src/mol-task/execution/runtime-context.ts
@@ -8,6 +8,7 @@ import { Task } from '../task'
 
 interface RuntimeContext {
     readonly shouldUpdate: boolean,
+    readonly isSynchronous: boolean,
 
     // Idiomatic usage:
     // if (ctx.needsYield) await ctx.yield({ ... });
diff --git a/src/mol-task/execution/synchronous.ts b/src/mol-task/execution/synchronous.ts
index 74b1180b51a7274afa5887aef5d9e3e6eeba243a..4ca40a04a5ccc91024b08ca3bcf6093ac8643e34 100644
--- a/src/mol-task/execution/synchronous.ts
+++ b/src/mol-task/execution/synchronous.ts
@@ -8,7 +8,9 @@ import { Task } from '../task'
 import { RuntimeContext } from './runtime-context'
 
 class SynchronousRuntimeContext implements RuntimeContext {
-    shouldUpdate: boolean = false;
+    shouldUpdate = false;
+    isSynchronous = true;
+
     update(progress: string | Partial<RuntimeContext.ProgressUpdate>, dontNotify?: boolean): Promise<void> | void { }
     runChild<T>(task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> { return ExecuteSynchronous(task); }
 }
diff --git a/src/mol-task/index.ts b/src/mol-task/index.ts
index 66449f0fb16c3e632963118f26e69d1604e83eed..830153c1cfaa3cd6512b083dd988b90b825bdb08 100644
--- a/src/mol-task/index.ts
+++ b/src/mol-task/index.ts
@@ -11,6 +11,8 @@ import { ExecuteObservable } from './execution/observable'
 import { Progress } from './execution/progress'
 import { now } from './util/now'
 import { Scheduler } from './util/scheduler'
+import { MultistepTask } from './util/multistep'
+import { chunkedSubtask } from './util/chunked'
 
 // Run the task without the ability to observe its progress.
 function Run<T>(task: Task<T>): Promise<T>;
@@ -21,4 +23,4 @@ function Run<T>(task: Task<T>, observer?: Progress.Observer, updateRateMs?: numb
     return ExecuteSynchronous(task);
 }
 
-export { Task, RuntimeContext, Progress, Run, now, Scheduler }
\ No newline at end of file
+export { Task, RuntimeContext, Progress, Run, now, Scheduler, MultistepTask, chunkedSubtask }
\ No newline at end of file
diff --git a/src/mol-task/util.ts b/src/mol-task/util.ts
deleted file mode 100644
index a3ce20ff33975a7f205ce180c2f377ab65e4715b..0000000000000000000000000000000000000000
--- a/src/mol-task/util.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-
-// enum TaskState {
-//     Pending,
-//     Running,
-//     Aborted,
-//     Completed,
-//     Failed
-// }
-
-interface TaskState {
-
-}
-
-namespace TaskState {
-    export interface Pending { kind: 'Pending' }
-    export interface Running { kind: 'Running',  }
-
-    export interface Progress {
-        message: string,
-        isIndeterminate: boolean,
-        current: number,
-        max: number,
-        elapsedMs: number
-    }
-}
-
-type ExecutionContext = {
-    run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>,
-    subscribe(o: (p: string, compId: number) => void): void,
-
-    requestAbort(compId: number): void
-}
-
-namespace ExecutionContext {
-    // export interface Synchronous extends ExecutionContext {
-    //     run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>,
-    // }
-
-    // export interface Observable extends ExecutionContext {
-    //     run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>,
-    // }
-    export const Sync: ExecutionContext = 0 as any;
-}
-
-interface RuntimeContext {
-    run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>,
-    yield(name: string): Promise<void> | void
-}
-
-// if no context is specified, use the synchronous one.
-interface Computation<T> { (ctx: RuntimeContext): Promise<T>, _id: number }
-
-function create<T>(c: (ctx: RuntimeContext) => Promise<T>): Computation<T> { return 0 as any; }
-function constant<T>(c: T) { return create(async ctx => c); }
-
-type MultistepFn<P, T> = (params: P, step: (s: number) => Promise<void> | void, ctx: RuntimeContext) => Promise<T>
-type ComputationProvider<P, T> = (params: P) => Computation<T>
-function MultistepComputation<P, T>(name: string, steps: string[], f: MultistepFn<P, T>): ComputationProvider<P, T> {
-    return params => create(async ctx => f(params, n => ctx.yield(steps[n]), ctx));
-}
-
-// if total count is specified, could automatically provide percentage
-type UniformlyChunkedFn<S> = (chunkSize: number, state: S, totalCount?: number) => number
-type UniformlyChunkedProvider<S> = (ctx: RuntimeContext, state: S) => Promise<S>
-function UniformlyChunked<S>(label: string, initialChunk: number, f: UniformlyChunkedFn<S>): UniformlyChunkedProvider<S> {
-    // TODO: track average time required for single element and then determine chunk size based on that.
-    return 0 as any;
-}
-
-type LineReaderState = { str: string, position: number, lines: string[] }
-const uniformPart = UniformlyChunked('Reading lines', 1000000, (size, state: LineReaderState) => {
-    state.position += size;
-    state.lines.push('');
-    return 0 /* number of lines read */;
-});
-
-function readLines(str: string): Computation<string[]> {
-    return create(async ctx => {
-        const state = (await uniformPart(ctx, { str, position: 0, lines: [] }));
-        return state.lines;
-    });
-}
-
-const prependHiToLines = MultistepComputation('Hi prepend', ['Parse input', 'Prepend Hi'], async (p: string, step, ctx) => {
-    await step(0);
-    const lines = await readLines(p)(ctx);
-    await step(1);
-    const ret = lines.map(l => 'Hi ' + l);
-    return ret;
-});
-
-
-(async function() {
-    const r = await ExecutionContext.Sync.run(prependHiToLines('1\n2'), { updateRateMs: 150 });
-    console.log(r)
-}())
diff --git a/src/mol-task/util/chunked.ts b/src/mol-task/util/chunked.ts
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d091e57066cbda2c53c38d14b0d18a2df53d4c8f 100644
--- a/src/mol-task/util/chunked.ts
+++ b/src/mol-task/util/chunked.ts
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { now } from './now'
+import { RuntimeContext } from '../execution/runtime-context'
+
+type UniformlyChunkedFn<S> = (chunkSize: number, state: S) => number
+
+async function chunkedSubtask<S>(ctx: RuntimeContext, initialChunk: number, state: S,
+    f: UniformlyChunkedFn<S>, update: (ctx: RuntimeContext, state: S, processed: number) => Promise<void> | void): Promise<S> {
+    let chunkSize = Math.max(initialChunk, 0);
+    let globalProcessed = 0, globalTime = 0;
+
+    if (ctx.isSynchronous) {
+        f(Number.MAX_SAFE_INTEGER, state);
+        return state;
+    }
+
+    let start = now();
+    let lastSize = 0, currentTime = 0;
+
+    while ((lastSize = f(chunkSize, state)) > 0) {
+        globalProcessed += lastSize;
+
+        const delta = now() - start;
+        currentTime += delta;
+        globalTime += delta;
+
+        if (ctx.shouldUpdate) {
+            await update(ctx, state, globalProcessed);
+
+            chunkSize = Math.round(currentTime * globalProcessed / globalTime) + 1;
+            start = now();
+            currentTime = 0;
+        }
+    }
+    if (ctx.shouldUpdate) {
+        await update(ctx, state, globalProcessed);
+    }
+    return state;
+}
+
+export { chunkedSubtask }
\ No newline at end of file
diff --git a/src/mol-task/util/multistep.ts b/src/mol-task/util/multistep.ts
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..dba09e8485627d5e3c08bbf703ca6af7994df750 100644
--- a/src/mol-task/util/multistep.ts
+++ b/src/mol-task/util/multistep.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from '../task'
+import { RuntimeContext } from '../execution/runtime-context'
+
+export type MultistepFn<P, T> =
+    (params: P, step: (s: number) => Promise<void> | void, ctx: RuntimeContext) => Promise<T>
+
+function MultistepTask<P, T>(name: string, steps: string[], f: MultistepFn<P, T>, onAbort?: () => void) {
+    return (params: P) => Task.create(name, async ctx => f(params, n => ctx.update({ message: `${steps[n]}`, current: n + 1, max: steps.length }), ctx), onAbort);
+}
+
+export { MultistepTask }
\ No newline at end of file
diff --git a/src/mol-util/computation.ts b/src/mol-util/computation.ts
deleted file mode 100644
index 3d956de70054e16b858a96c080393ac582517ab7..0000000000000000000000000000000000000000
--- a/src/mol-util/computation.ts
+++ /dev/null
@@ -1,283 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * Adapted from https://github.com/dsehnal/LiteMol
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import Scheduler from './scheduler'
-import timeNow from './time'
-
-interface Computation<A> {
-    (ctx?: Computation.Context): Promise<A>
-}
-
-namespace Computation {
-    export let PRINT_ERRORS_TO_CONSOLE = false;
-
-    export function create<A>(computation: (ctx: Context) => Promise<A>) {
-        return ComputationImpl(computation);
-    }
-
-    export function resolve<A>(a: A) {
-        return create<A>(_ => Promise.resolve(a));
-    }
-
-    export function reject<A>(reason: any) {
-        return create<A>(_ => Promise.reject(reason));
-    }
-
-    export interface Params {
-        updateRateMs?: number,
-        observer?: ProgressObserver
-    }
-
-    export const Aborted = 'Aborted';
-
-    export interface Progress {
-        message: string,
-        isIndeterminate: boolean,
-        current: number,
-        max: number,
-        elapsedMs: number,
-        requestAbort?: () => void
-    }
-
-    export interface ProgressUpdate {
-        message?: string,
-        abort?: boolean | (() => void),
-        current?: number,
-        max?: number
-    }
-
-    export interface Context {
-        readonly isSynchronous: boolean,
-        /** Also checks if the computation was aborted. If so, throws. */
-        readonly requiresUpdate: boolean,
-        requestAbort(): void,
-
-        subscribe(onProgress: ProgressObserver): { dispose: () => void },
-        /** Also checks if the computation was aborted. If so, throws. */
-        update(info: ProgressUpdate): Promise<void> | void
-    }
-
-    export type ProgressObserver = (progress: Readonly<Progress>) => void;
-
-    const emptyDisposer = { dispose: () => { } }
-
-    /** A context without updates. */
-    export const synchronous: Context = {
-        isSynchronous: true,
-        requiresUpdate: false,
-        requestAbort() { },
-        subscribe(onProgress) { return emptyDisposer; },
-        update(info) { }
-    }
-
-    export function observable(params?: Partial<Params>) {
-        const ret = new ObservableContext(params && params.updateRateMs);
-        if (params && params.observer) ret.subscribe(params.observer);
-        return ret;
-    }
-
-    export const now = timeNow;
-
-    /** A utility for splitting large computations into smaller parts. */
-    export interface Chunker {
-        setNextChunkSize(size: number): void,
-        /** nextChunk must return the number of actually processed chunks. */
-        process(nextChunk: (chunkSize: number) => number, update: (updater: Context['update']) => void, nextChunkSize?: number): Promise<void>
-    }
-
-    export function chunker(ctx: Context, nextChunkSize: number): Chunker {
-        return new ChunkerImpl(ctx, nextChunkSize);
-    }
-}
-
-const DefaulUpdateRateMs = 150;
-
-function ComputationImpl<A>(computation: (ctx: Computation.Context) => Promise<A>): Computation<A> {
-    return (ctx?: Computation.Context) => {
-        const context: ObservableContext = ctx ? ctx : Computation.synchronous as any;
-        return new Promise<A>(async (resolve, reject) => {
-            try {
-                if (context.started) context.started();
-                const result = await computation(context);
-                resolve(result);
-            } catch (e) {
-                if (Computation.PRINT_ERRORS_TO_CONSOLE) console.error(e);
-                reject(e);
-            } finally {
-                if (context.finished) context.finished();
-            }
-        });
-    }
-}
-
-class ObservableContext implements Computation.Context {
-    readonly updateRate: number;
-    readonly isSynchronous: boolean = false;
-    private level = 0;
-    private startedTime = 0;
-    private abortRequested = false;
-    private lastUpdated = 0;
-    private observers: Computation.ProgressObserver[] | undefined = void 0;
-    private progress: Computation.Progress = { message: 'Working...', current: 0, max: 0, elapsedMs: 0, isIndeterminate: true, requestAbort: void 0 };
-
-    private checkAborted() {
-        if (this.abortRequested) throw Computation.Aborted;
-    }
-
-    private abortRequester = () => { this.abortRequested = true };
-
-    subscribe = (obs: Computation.ProgressObserver) => {
-        if (!this.observers) this.observers = [];
-        this.observers.push(obs);
-        return {
-            dispose: () => {
-                if (!this.observers) return;
-                for (let i = 0; i < this.observers.length; i++) {
-                    if (this.observers[i] === obs) {
-                        this.observers[i] = this.observers[this.observers.length - 1];
-                        this.observers.pop();
-                        return;
-                    }
-                }
-            }
-        };
-    }
-
-    requestAbort() {
-        try {
-            if (this.abortRequester) {
-                this.abortRequester.call(null);
-            }
-        } catch (e) { }
-    }
-
-    update({ message, abort, current, max }: Computation.ProgressUpdate): Promise<void> | void {
-        this.checkAborted();
-
-        const time = Computation.now();
-
-        if (typeof abort === 'boolean') {
-            this.progress.requestAbort = abort ? this.abortRequester : void 0;
-        } else {
-            if (abort) this.abortRequester = abort;
-            this.progress.requestAbort = abort ? this.abortRequester : void 0;
-        }
-
-        if (typeof message !== 'undefined') this.progress.message = message;
-        this.progress.elapsedMs = time - this.startedTime;
-        if (isNaN(current!)) {
-            this.progress.isIndeterminate = true;
-        } else {
-            this.progress.isIndeterminate = false;
-            this.progress.current = current!;
-            if (!isNaN(max!)) this.progress.max = max!;
-        }
-
-        if (this.observers) {
-            const p = { ...this.progress };
-            for (let i = 0, _i = this.observers.length; i < _i; i++) {
-                Scheduler.immediate(this.observers[i], p);
-            }
-        }
-
-        this.lastUpdated = time;
-
-        return Scheduler.immediatePromise();
-    }
-
-    get requiresUpdate() {
-        this.checkAborted();
-        if (this.isSynchronous) return false;
-        return Computation.now() - this.lastUpdated > this.updateRate;
-    }
-
-    started() {
-        if (!this.level) {
-            this.startedTime = Computation.now();
-            this.lastUpdated = this.startedTime;
-        }
-        this.level++;
-    }
-
-    finished() {
-        this.level--;
-        if (this.level < 0) {
-            throw new Error('Bug in code somewhere, Computation.resolve/reject called too many times.');
-        }
-        if (!this.level) this.observers = void 0;
-    }
-
-    constructor(updateRate?: number) {
-        this.updateRate = updateRate || DefaulUpdateRateMs;
-    }
-}
-
-class ChunkerImpl implements Computation.Chunker {
-    private processedSinceUpdate = 0;
-    private updater: Computation.Context['update'];
-
-    private computeChunkSize(delta: number) {
-        if (!delta) {
-            this.processedSinceUpdate = 0;
-            return this.nextChunkSize;
-        }
-        const rate = (this.context as ObservableContext).updateRate || DefaulUpdateRateMs;
-        const ret = Math.round(this.processedSinceUpdate * rate / delta + 1);
-        this.processedSinceUpdate = 0;
-        return ret;
-    }
-
-    private getNextChunkSize() {
-        const ctx = this.context as ObservableContext;
-        // be smart if the computation is synchronous and process the whole chunk at once.
-        if (ctx.isSynchronous) return Number.MAX_SAFE_INTEGER;
-        return this.nextChunkSize;
-    }
-
-    setNextChunkSize(size: number) {
-        this.nextChunkSize = size;
-    }
-
-    async process(nextChunk: (size: number) => number, update: (updater: Computation.Context['update']) => Promise<void> | void, nextChunkSize?: number) {
-        if (typeof nextChunkSize !== 'undefined') this.setNextChunkSize(nextChunkSize);
-        this.processedSinceUpdate = 0;
-
-        // track time for the actual computation and exclude the "update time"
-        let chunkStart = Computation.now();
-        let lastChunkSize: number;
-        let chunkCount = 0;
-        let totalSize = 0;
-        let updateCount = 0;
-        while ((lastChunkSize = nextChunk(this.getNextChunkSize())) > 0) {
-            chunkCount++;
-            this.processedSinceUpdate += lastChunkSize;
-            totalSize += lastChunkSize;
-            if (this.context.requiresUpdate) {
-                let time = Computation.now();
-                await update(this.updater);
-                this.nextChunkSize = updateCount > 0
-                    ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1))
-                    : this.computeChunkSize(time - chunkStart)
-                updateCount++;
-                chunkStart = Computation.now();
-            }
-        }
-        if (this.context.requiresUpdate) {
-            let time = Computation.now();
-            await update(this.updater);
-            this.nextChunkSize = updateCount > 0
-                ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1))
-                : this.computeChunkSize(time - chunkStart)
-        }
-    }
-
-    constructor(public context: Computation.Context, private nextChunkSize: number) {
-        this.updater = this.context.update.bind(this.context);
-    }
-}
-
-export default Computation;
\ No newline at end of file
diff --git a/src/mol-util/index.ts b/src/mol-util/index.ts
index caa3e33f456784b6a966406074d5726e92c1b190..16d00860d8b51105c8e5fd8ac951091612ad895e 100644
--- a/src/mol-util/index.ts
+++ b/src/mol-util/index.ts
@@ -6,13 +6,12 @@
  */
 
 import BitFlags from './bit-flags'
-import Computation from './computation'
-import Scheduler from './scheduler'
 import StringBuilder from './string-builder'
-import Time from './time'
 import UUID from './uuid'
+import Mask from './mask'
 
-export { BitFlags, Computation, Scheduler, StringBuilder, Time, UUID }
+export * from './value-cell'
+export { BitFlags, StringBuilder, UUID, Mask }
 
 export function arrayEqual<T>(arr1: T[], arr2: T[]) {
     const length = arr1.length
diff --git a/src/mol-util/scheduler.ts b/src/mol-util/scheduler.ts
deleted file mode 100644
index e118ec97cbf221e6b21d7126687f39bcc78753e7..0000000000000000000000000000000000000000
--- a/src/mol-util/scheduler.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-/**
- * setImmediate polyfill adapted from https://github.com/YuzuJS/setImmediate
- * Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola
- * MIT license.
- */
-
-function createImmediateActions() {
-    type Callback = (...args: any[]) => void;
-    type Task = { callback: Callback, args: any[] }
-
-    const tasksByHandle: { [handle: number]: Task } = { };
-    const doc = typeof document !== 'undefined' ? document : void 0;
-
-    let currentlyRunningATask = false;
-    let nextHandle = 1; // Spec says greater than zero
-    let registerImmediate: ((handle: number) => void);
-
-    function setImmediate(callback: Callback, ...args: any[]) {
-      // Callback can either be a function or a string
-      if (typeof callback !== 'function') {
-        callback = new Function('' + callback) as Callback;
-      }
-      // Store and register the task
-      const task = { callback: callback, args: args };
-      tasksByHandle[nextHandle] = task;
-      registerImmediate(nextHandle);
-      return nextHandle++;
-    }
-
-    function clearImmediate(handle: number) {
-        delete tasksByHandle[handle];
-    }
-
-    function run(task: Task) {
-        const callback = task.callback;
-        const args = task.args;
-        switch (args.length) {
-        case 0:
-            callback();
-            break;
-        case 1:
-            callback(args[0]);
-            break;
-        case 2:
-            callback(args[0], args[1]);
-            break;
-        case 3:
-            callback(args[0], args[1], args[2]);
-            break;
-        default:
-            callback.apply(undefined, args);
-            break;
-        }
-    }
-
-    function runIfPresent(handle: number) {
-        // From the spec: 'Wait until any invocations of this algorithm started before this one have completed.'
-        // So if we're currently running a task, we'll need to delay this invocation.
-        if (currentlyRunningATask) {
-            // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a
-            // 'too much recursion' error.
-            setTimeout(runIfPresent, 0, handle);
-        } else {
-            const task = tasksByHandle[handle];
-            if (task) {
-                currentlyRunningATask = true;
-                try {
-                    run(task);
-                } finally {
-                    clearImmediate(handle);
-                    currentlyRunningATask = false;
-                }
-            }
-        }
-    }
-
-    function installNextTickImplementation() {
-        registerImmediate = function(handle) {
-            process.nextTick(function () { runIfPresent(handle); });
-        };
-    }
-
-    function canUsePostMessage() {
-        // The test against `importScripts` prevents this implementation from being installed inside a web worker,
-        // where `global.postMessage` means something completely different and can't be used for this purpose.
-        const global = typeof window !== 'undefined' ? window as any : void 0;
-        if (global && global.postMessage && !global.importScripts) {
-            let postMessageIsAsynchronous = true;
-            const oldOnMessage = global.onmessage;
-            global.onmessage = function() {
-                postMessageIsAsynchronous = false;
-            };
-            global.postMessage('', '*');
-            global.onmessage = oldOnMessage;
-            return postMessageIsAsynchronous;
-        }
-    }
-
-    function installPostMessageImplementation() {
-        // Installs an event handler on `global` for the `message` event: see
-        // * https://developer.mozilla.org/en/DOM/window.postMessage
-        // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
-
-        const messagePrefix = 'setImmediate$' + Math.random() + '$';
-        const global = typeof window !== 'undefined' ? window as any : void 0;
-        const onGlobalMessage = function(event: any) {
-            if (event.source === global &&
-                typeof event.data === 'string' &&
-                event.data.indexOf(messagePrefix) === 0) {
-                runIfPresent(+event.data.slice(messagePrefix.length));
-            }
-        };
-
-        if (window.addEventListener) {
-            window.addEventListener('message', onGlobalMessage, false);
-        } else {
-            (window as any).attachEvent('onmessage', onGlobalMessage);
-        }
-
-        registerImmediate = function(handle) {
-            window.postMessage(messagePrefix + handle, '*');
-        };
-    }
-
-    function installMessageChannelImplementation() {
-        const channel = new MessageChannel();
-        channel.port1.onmessage = function(event) {
-            const handle = event.data;
-            runIfPresent(handle);
-        };
-
-        registerImmediate = function(handle) {
-            channel.port2.postMessage(handle);
-        };
-    }
-
-    function installReadyStateChangeImplementation() {
-        const html = doc!.documentElement;
-        registerImmediate = function(handle) {
-            // Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
-            // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
-            let script = doc!.createElement('script') as any;
-            script.onreadystatechange = function () {
-                runIfPresent(handle);
-                script.onreadystatechange = null;
-                html.removeChild(script);
-                script = null;
-            };
-            html.appendChild(script);
-        };
-    }
-
-    function installSetTimeoutImplementation() {
-        registerImmediate = function(handle) {
-            setTimeout(runIfPresent, 0, handle);
-        };
-    }
-
-    // Don't get fooled by e.g. browserify environments.
-    if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') {
-        // For Node.js before 0.9
-        installNextTickImplementation();
-    } else if (canUsePostMessage()) {
-        // For non-IE10 modern browsers
-        installPostMessageImplementation();
-    } else if (typeof MessageChannel !== 'undefined') {
-        // For web workers, where supported
-        installMessageChannelImplementation();
-    } else if (doc && 'onreadystatechange' in doc.createElement('script')) {
-        // For IE 6–8
-        installReadyStateChangeImplementation();
-    } else {
-        // For older browsers
-        installSetTimeoutImplementation();
-    }
-
-    return {
-        setImmediate,
-        clearImmediate
-    };
-}
-
-const immediateActions = (function () {
-    if (typeof setImmediate !== 'undefined') {
-        if (typeof window !== 'undefined') {
-            return { setImmediate: (handler: any, ...args: any[]) => window.setImmediate(handler, ...args as any), clearImmediate: (handle: any) => window.clearImmediate(handle) };
-        } else return { setImmediate, clearImmediate }
-    }
-    return createImmediateActions();
-}());
-
-function resolveImmediate(res: () => void) {
-    immediateActions.setImmediate(res);
-}
-
-export default {
-    immediate: immediateActions.setImmediate,
-    clearImmediate: immediateActions.clearImmediate,
-
-    immediatePromise() { return new Promise<void>(resolveImmediate); }
-};
diff --git a/src/mol-util/time.ts b/src/mol-util/time.ts
deleted file mode 100644
index a0ee3abfdeccd9b60ee442d460e49b2f496f7813..0000000000000000000000000000000000000000
--- a/src/mol-util/time.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-declare var process: any;
-declare var window: any;
-
-const now: () => number = (function () {
-    if (typeof window !== 'undefined' && window.performance) {
-        const perf = window.performance;
-        return () => perf.now();
-    } else if (typeof process !== 'undefined' && process.hrtime !== 'undefined') {
-        return () => {
-            let t = process.hrtime();
-            return t[0] * 1000 + t[1] / 1000000;
-        };
-    } else {
-        return () => +new Date();
-    }
-}());
-
-export default now;
\ No newline at end of file
diff --git a/src/mol-util/uuid.ts b/src/mol-util/uuid.ts
index 471d349695e2ff619c8166a5066fbffc539ef652..b3dc51a34c377aa27e53e7971456f061b163bb2d 100644
--- a/src/mol-util/uuid.ts
+++ b/src/mol-util/uuid.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import now from './time'
+import { now } from 'mol-task'
 
 interface UUID extends String { '@type': 'uuid' }
 
diff --git a/src/mol-util/value-cell.ts b/src/mol-util/value-cell.ts
new file mode 100644
index 0000000000000000000000000000000000000000..762bcbf7e723970b2f755d6658cddc3efba52675
--- /dev/null
+++ b/src/mol-util/value-cell.ts
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+/** A mutable value cell. */
+interface ValueCell<T> { value: T }
+/** Create a mutable value cell. */
+function ValueCell<T>(value: T): ValueCell<T> { return { value }; }
+
+/** An immutable value box that also holds a version of the attribute. */
+interface ValueBox<T> { readonly version: number, readonly value: T }
+/** Create a new box with the specified value and version = 0 */
+function ValueBox<T>(value: T): ValueBox<T>
+/** Create a new box by updating the value of an old box and incrementing the version number. */
+function ValueBox<T>(box: ValueBox<T>, value: T): ValueBox<T>
+function ValueBox<T>(boxOrValue: T | ValueBox<T>, value?: T): ValueBox<T> {
+    if (arguments.length === 2) return { version: (boxOrValue as ValueBox<T>).version + 1, value: value! };
+    return { version: 0, value: boxOrValue as T };
+}
+
+export { ValueCell, ValueBox };
+
diff --git a/src/perf-tests/structure.ts b/src/perf-tests/structure.ts
index 01df5dc9d5f2a7b13045f1fe7180b04bef13708e..98111e1d358200da7964a89f6d72f829b0e8e891 100644
--- a/src/perf-tests/structure.ts
+++ b/src/perf-tests/structure.ts
@@ -15,6 +15,7 @@ import { Structure, Model, Queries as Q, Atom, AtomGroup, AtomSet, Selection, Sy
 import { Segmentation } from 'mol-data/int'
 
 import to_mmCIF from 'mol-model/structure/export/mmcif'
+import { Run } from 'mol-task';
 
 require('util.promisify').shim();
 const readFileAsync = util.promisify(fs.readFile);
@@ -59,7 +60,7 @@ export async function readCIF(path: string) {
 
     console.time('parse');
     const comp = typeof input === 'string' ? CIF.parseText(input) : CIF.parseBinary(input);
-    const parsed = await comp();
+    const parsed = await Run(comp);
     console.timeEnd('parse');
     if (parsed.isError) {
         throw parsed;