septima-lang 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ast-node.ts CHANGED
@@ -1,9 +1,22 @@
1
1
  import { Span } from './location'
2
2
  import { Token } from './scanner'
3
3
  import { shouldNeverHappen } from './should-never-happen'
4
+ import { switchOn } from './switch-on'
4
5
 
5
6
  export type Let = { start: Token; ident: Ident; value: AstNode }
6
7
 
8
+ export type Import = {
9
+ start: Token
10
+ ident: Ident
11
+ pathToImportFrom: Token
12
+ }
13
+
14
+ export type Literal = {
15
+ tag: 'literal'
16
+ type: 'str' | 'bool' | 'num' | 'sink' | 'sink!' | 'sink!!'
17
+ t: Token
18
+ }
19
+
7
20
  export type ObjectLiteralPart =
8
21
  | { tag: 'hardName'; k: Ident; v: AstNode }
9
22
  | { tag: 'computedName'; k: AstNode; v: AstNode }
@@ -23,8 +36,16 @@ export type Lambda = {
23
36
  body: AstNode
24
37
  }
25
38
 
39
+ export type Unit = {
40
+ tag: 'unit'
41
+ imports: Import[]
42
+ expression: AstNode
43
+ }
44
+
26
45
  export type AstNode =
27
46
  | Ident
47
+ | Literal
48
+ | Unit
28
49
  | {
29
50
  start: Token
30
51
  tag: 'arrayLiteral'
@@ -37,11 +58,6 @@ export type AstNode =
37
58
  parts: ObjectLiteralPart[]
38
59
  end: Token
39
60
  }
40
- | {
41
- tag: 'literal'
42
- type: 'str' | 'bool' | 'num' | 'sink' | 'sink!' | 'sink!!'
43
- t: Token
44
- }
45
61
  | {
46
62
  tag: 'binaryOperator'
47
63
  operator: '+' | '-' | '*' | '/' | '**' | '%' | '&&' | '||' | '>' | '<' | '>=' | '<=' | '==' | '!=' | '??'
@@ -82,6 +98,12 @@ export type AstNode =
82
98
  receiver: AstNode
83
99
  index: AstNode
84
100
  }
101
+ | {
102
+ // A sepcial AST node meant to be generated internally (needed for exporting definition from one unit to another).
103
+ // Not intended to be parsed from source code. Hence, it is effectively empty, and its location cannot be
104
+ // determined.
105
+ tag: 'export*'
106
+ }
85
107
 
86
108
  export function show(ast: AstNode | AstNode[]): string {
87
109
  if (Array.isArray(ast)) {
@@ -105,11 +127,12 @@ export function show(ast: AstNode | AstNode[]): string {
105
127
  if (ast.tag === 'binaryOperator') {
106
128
  return `(${show(ast.lhs)} ${ast.operator} ${show(ast.rhs)})`
107
129
  }
108
-
109
130
  if (ast.tag === 'dot') {
110
131
  return `${show(ast.receiver)}.${show(ast.ident)}`
111
132
  }
112
-
133
+ if (ast.tag === 'export*') {
134
+ return `(export*)`
135
+ }
113
136
  if (ast.tag === 'functionCall') {
114
137
  return `${show(ast.callee)}(${show(ast.actualArgs)})`
115
138
  }
@@ -126,7 +149,14 @@ export function show(ast: AstNode | AstNode[]): string {
126
149
  return `fun (${show(ast.formalArgs)}) ${show(ast.body)}`
127
150
  }
128
151
  if (ast.tag === 'literal') {
129
- return ast.t.text
152
+ return switchOn(ast.type, {
153
+ bool: () => ast.t.text,
154
+ num: () => ast.t.text,
155
+ sink: () => 'sink',
156
+ 'sink!': () => 'sink!',
157
+ 'sink!!': () => 'sink!!',
158
+ str: () => `'${ast.t.text}'`,
159
+ })
130
160
  }
131
161
  if (ast.tag === 'objectLiteral') {
132
162
  const pairs = ast.parts.map(p => {
@@ -153,6 +183,12 @@ export function show(ast: AstNode | AstNode[]): string {
153
183
  if (ast.tag === 'unaryOperator') {
154
184
  return `${ast.operator}${show(ast.operand)}`
155
185
  }
186
+ if (ast.tag === 'unit') {
187
+ const imports = ast.imports
188
+ .map(imp => `import * as ${show(imp.ident)} from '${imp.pathToImportFrom.text}';`)
189
+ .join('\n')
190
+ return `${imports ? imports + '\n' : ''}${show(ast.expression)}`
191
+ }
156
192
 
157
193
  shouldNeverHappen(ast)
158
194
  }
@@ -176,6 +212,9 @@ export function span(ast: AstNode): Span {
176
212
  if (ast.tag === 'ident') {
177
213
  return ofToken(ast.t)
178
214
  }
215
+ if (ast.tag === 'export*') {
216
+ return { from: { offset: 0 }, to: { offset: 0 } }
217
+ }
179
218
  if (ast.tag === 'if') {
180
219
  return ofRange(span(ast.condition), span(ast.negative))
181
220
  }
@@ -200,6 +239,11 @@ export function span(ast: AstNode): Span {
200
239
  if (ast.tag === 'unaryOperator') {
201
240
  return ofRange(ofToken(ast.operatorToken), span(ast.operand))
202
241
  }
242
+ if (ast.tag === 'unit') {
243
+ const i0 = ast.imports.find(Boolean)
244
+ const exp = span(ast.expression)
245
+ return ofRange(i0 ? ofToken(i0.start) : exp, exp)
246
+ }
203
247
 
204
248
  shouldNeverHappen(ast)
205
249
  }
package/src/parser.ts CHANGED
@@ -1,17 +1,58 @@
1
- import { ArrayLiteralPart, AstNode, Ident, Let, ObjectLiteralPart } from './ast-node'
1
+ import { ArrayLiteralPart, AstNode, Ident, Import, Let, Literal, ObjectLiteralPart, span, Unit } from './ast-node'
2
2
  import { Scanner, Token } from './scanner'
3
+ import { switchOn } from './switch-on'
3
4
 
4
5
  export class Parser {
5
6
  constructor(private readonly scanner: Scanner) {}
6
7
 
7
- parse() {
8
- const ret = this.expression()
8
+ parse(): Unit {
9
+ const ret = this.unit()
9
10
  if (!this.scanner.eof()) {
10
11
  throw new Error(`Loitering input ${this.scanner.sourceRef}`)
11
12
  }
12
13
  return ret
13
14
  }
14
15
 
16
+ unit(): Unit {
17
+ const imports = this.imports()
18
+ const expression = this.expression()
19
+ return { tag: 'unit', imports, expression }
20
+ }
21
+
22
+ imports(): Import[] {
23
+ const ret: Import[] = []
24
+ while (true) {
25
+ const start = this.scanner.consumeIf('import')
26
+ if (!start) {
27
+ return ret
28
+ }
29
+
30
+ this.scanner.consume('*')
31
+ this.scanner.consume('as')
32
+ const ident = this.identifier()
33
+ this.scanner.consume('from')
34
+ const pathToImportFrom = this.maybePrimitiveLiteral()
35
+ if (pathToImportFrom === undefined) {
36
+ throw new Error(`Expected a literal ${this.scanner.sourceRef}`)
37
+ }
38
+
39
+ const notString = () => {
40
+ throw new Error(`Expected a string literal ${this.scanner.sourceCode.sourceRef(span(pathToImportFrom))}`)
41
+ }
42
+ switchOn(pathToImportFrom.type, {
43
+ bool: notString,
44
+ num: notString,
45
+ str: () => {},
46
+ sink: notString,
47
+ 'sink!': notString,
48
+ 'sink!!': notString,
49
+ })
50
+ ret.push({ start, ident, pathToImportFrom: pathToImportFrom.t })
51
+
52
+ this.scanner.consumeIf(';')
53
+ }
54
+ }
55
+
15
56
  definitions(): Let[] {
16
57
  const ret: Let[] = []
17
58
  while (true) {
@@ -310,6 +351,18 @@ export class Parser {
310
351
  }
311
352
 
312
353
  literalOrIdent(): AstNode {
354
+ const ret = this.maybeLiteral() ?? this.maybeIdentifier()
355
+ if (!ret) {
356
+ throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
357
+ }
358
+ return ret
359
+ }
360
+
361
+ maybeLiteral(): AstNode | undefined {
362
+ return this.maybePrimitiveLiteral() ?? this.maybeCompositeLiteral()
363
+ }
364
+
365
+ maybePrimitiveLiteral(): Literal | undefined {
313
366
  let t = this.scanner.consumeIf('sink!!')
314
367
  if (t) {
315
368
  return { tag: 'literal', type: 'sink!!', t }
@@ -353,7 +406,11 @@ export class Parser {
353
406
  return { tag: 'literal', type: 'str', t }
354
407
  }
355
408
 
356
- t = this.scanner.consumeIf('[')
409
+ return undefined
410
+ }
411
+
412
+ maybeCompositeLiteral(): AstNode | undefined {
413
+ let t = this.scanner.consumeIf('[')
357
414
  if (t) {
358
415
  return this.arrayBody(t)
359
416
  }
@@ -363,12 +420,7 @@ export class Parser {
363
420
  return this.objectBody(t)
364
421
  }
365
422
 
366
- const ident = this.maybeIdentifier()
367
- if (ident) {
368
- return ident
369
- }
370
-
371
- throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
423
+ return undefined
372
424
  }
373
425
 
374
426
  /**
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AstNode, show, span } from './ast-node'
1
+ import { AstNode, show, span, Unit } from './ast-node'
2
2
  import { extractMessage } from './extract-message'
3
3
  import { failMe } from './fail-me'
4
4
  import { shouldNeverHappen } from './should-never-happen'
@@ -31,6 +31,12 @@ class SymbolFrame implements SymbolTable {
31
31
  ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
32
32
  return ret
33
33
  }
34
+
35
+ exportValue(): Record<string, Value> {
36
+ const ret = this.earlier.exportValue()
37
+ ret[this.symbol] = this.placeholder.destination ?? failMe(`Unbounded symbol: ${this.symbol}`)
38
+ return ret
39
+ }
34
40
  }
35
41
 
36
42
  class EmptySymbolTable implements SymbolTable {
@@ -41,6 +47,10 @@ class EmptySymbolTable implements SymbolTable {
41
47
  export() {
42
48
  return {}
43
49
  }
50
+
51
+ exportValue(): Record<string, Value> {
52
+ return {}
53
+ }
44
54
  }
45
55
 
46
56
  export type Verbosity = 'quiet' | 'trace'
@@ -50,10 +60,11 @@ export class Runtime {
50
60
  constructor(
51
61
  private readonly root: AstNode,
52
62
  private readonly verbosity: Verbosity = 'quiet',
53
- private readonly preimports: Record<string, Value> = {},
63
+ private readonly preimports: Record<string, Value>,
64
+ private readonly getAstOf: (fileName: string) => Unit,
54
65
  ) {}
55
66
 
56
- compute() {
67
+ private buildInitialSymbolTable() {
57
68
  const empty = new EmptySymbolTable()
58
69
 
59
70
  const keys = Value.foreign(o => o.keys())
@@ -64,9 +75,12 @@ export class Runtime {
64
75
  for (const [importName, importValue] of Object.entries(this.preimports)) {
65
76
  lib = new SymbolFrame(importName, { destination: importValue }, lib)
66
77
  }
78
+ return lib
79
+ }
67
80
 
81
+ compute() {
68
82
  try {
69
- const value = this.evalNode(this.root, lib)
83
+ const value = this.evalNode(this.root, this.buildInitialSymbolTable())
70
84
  return { value }
71
85
  } catch (e) {
72
86
  const trace: AstNode[] = []
@@ -99,7 +113,50 @@ export class Runtime {
99
113
  return ret
100
114
  }
101
115
 
116
+ private importDefinitions(pathToImportFrom: string): Value {
117
+ const ast = this.getAstOf(pathToImportFrom)
118
+ const exp = ast.expression
119
+ const imports = ast.imports
120
+ if (
121
+ exp.tag === 'arrayLiteral' ||
122
+ exp.tag === 'binaryOperator' ||
123
+ exp.tag === 'dot' ||
124
+ exp.tag === 'export*' ||
125
+ exp.tag === 'functionCall' ||
126
+ exp.tag === 'ident' ||
127
+ exp.tag === 'if' ||
128
+ exp.tag === 'indexAccess' ||
129
+ exp.tag === 'lambda' ||
130
+ exp.tag === 'literal' ||
131
+ exp.tag === 'objectLiteral' ||
132
+ exp.tag === 'unaryOperator' ||
133
+ exp.tag === 'unit'
134
+ ) {
135
+ // TODO(imaman): throw an error on non-exporting unit?
136
+ return Value.obj({})
137
+ }
138
+
139
+ if (exp.tag === 'topLevelExpression') {
140
+ const unit: AstNode = {
141
+ tag: 'unit',
142
+ imports,
143
+ expression: { tag: 'topLevelExpression', definitions: exp.definitions, computation: { tag: 'export*' } },
144
+ }
145
+ return this.evalNode(unit, this.buildInitialSymbolTable())
146
+ }
147
+
148
+ shouldNeverHappen(exp)
149
+ }
150
+
102
151
  private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
152
+ if (ast.tag === 'unit') {
153
+ let newTable = table
154
+ for (const imp of ast.imports) {
155
+ const o = this.importDefinitions(imp.pathToImportFrom.text)
156
+ newTable = new SymbolFrame(imp.ident.t.text, { destination: o }, newTable)
157
+ }
158
+ return this.evalNode(ast.expression, newTable)
159
+ }
103
160
  if (ast.tag === 'topLevelExpression') {
104
161
  let newTable = table
105
162
  for (const def of ast.definitions) {
@@ -113,6 +170,10 @@ export class Runtime {
113
170
  return this.evalNode(ast.computation, newTable)
114
171
  }
115
172
 
173
+ if (ast.tag === 'export*') {
174
+ return Value.obj(table.exportValue())
175
+ }
176
+
116
177
  if (ast.tag === 'binaryOperator') {
117
178
  const lhs = this.evalNode(ast.lhs, table)
118
179
  if (ast.operator === '||') {
package/src/scanner.ts CHANGED
@@ -7,7 +7,7 @@ export interface Token {
7
7
  }
8
8
 
9
9
  export class Scanner {
10
- constructor(private readonly sourceCode: SourceCode, private offset = 0) {
10
+ constructor(readonly sourceCode: SourceCode, private offset = 0) {
11
11
  if (this.offset === 0) {
12
12
  this.eatWhitespace()
13
13
  }
package/src/septima.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Unit } from './ast-node'
1
2
  import { Parser } from './parser'
2
3
  import { Result, ResultSink, ResultSinkImpl } from './result'
3
4
  import { Runtime, Verbosity } from './runtime'
@@ -33,7 +34,7 @@ export class Septima {
33
34
  ((r: ResultSink) => {
34
35
  throw new Error(r.message)
35
36
  })
36
- const res = new Septima(input).compute()
37
+ const res = new Septima().compute(input)
37
38
  if (res.tag === 'ok') {
38
39
  return res.value
39
40
  }
@@ -45,13 +46,23 @@ export class Septima {
45
46
  shouldNeverHappen(res)
46
47
  }
47
48
 
48
- constructor(readonly input: string, private readonly preimports: Record<string, string> = {}) {}
49
+ constructor() {}
49
50
 
50
- compute(verbosity: Verbosity = 'quiet'): Result {
51
+ computeModule(moduleName: string, moduleReader: (m: string) => string): Result {
52
+ const input = moduleReader(moduleName)
53
+ const sourceCode = new SourceCode(input)
54
+ const value = this.computeImpl(sourceCode, 'quiet', {}, moduleReader)
55
+ if (!value.isSink()) {
56
+ return { value: value.export(), tag: 'ok' }
57
+ }
58
+ return new ResultSinkImpl(value, sourceCode)
59
+ }
60
+
61
+ compute(input: string, preimports: Record<string, string> = {}, verbosity: Verbosity = 'quiet'): Result {
51
62
  const lib: Record<string, Value> = {}
52
- for (const [importName, importCode] of Object.entries(this.preimports)) {
63
+ for (const [importName, importCode] of Object.entries(preimports)) {
53
64
  const sourceCode = new SourceCode(importCode)
54
- const value = this.computeImpl(sourceCode, verbosity, {})
65
+ const value = this.computeImpl(sourceCode, verbosity, {}, undefined)
55
66
  if (value.isSink()) {
56
67
  // TODO(imaman): cover!
57
68
  const r = new ResultSinkImpl(value, sourceCode)
@@ -60,20 +71,31 @@ export class Septima {
60
71
  lib[importName] = value
61
72
  }
62
73
 
63
- const sourceCode = new SourceCode(this.input)
64
- const value = this.computeImpl(sourceCode, verbosity, lib)
74
+ const sourceCode = new SourceCode(input)
75
+ const value = this.computeImpl(sourceCode, verbosity, lib, undefined)
65
76
  if (!value.isSink()) {
66
77
  return { value: value.export(), tag: 'ok' }
67
78
  }
68
79
  return new ResultSinkImpl(value, sourceCode)
69
80
  }
70
81
 
71
- private computeImpl(sourceCode: SourceCode, verbosity: Verbosity, lib: Record<string, Value>) {
82
+ private computeImpl(
83
+ sourceCode: SourceCode,
84
+ verbosity: Verbosity,
85
+ lib: Record<string, Value>,
86
+ moduleReader: undefined | ((m: string) => string),
87
+ ) {
72
88
  const scanner = new Scanner(sourceCode)
73
89
  const parser = new Parser(scanner)
74
-
75
90
  const ast = parse(parser)
76
- const runtime = new Runtime(ast, verbosity, lib)
91
+
92
+ const getAstOf = (fileName: string) => {
93
+ if (!moduleReader) {
94
+ throw new Error(`cannot read modules`)
95
+ }
96
+ return parse(moduleReader(fileName))
97
+ }
98
+ const runtime = new Runtime(ast, verbosity, lib, getAstOf)
77
99
  const c = runtime.compute()
78
100
 
79
101
  if (c.value) {
@@ -85,7 +107,7 @@ export class Septima {
85
107
  }
86
108
  }
87
109
 
88
- export function parse(arg: string | Parser) {
110
+ export function parse(arg: string | Parser): Unit {
89
111
  const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
90
112
  const ast = parser.parse()
91
113
  return ast
@@ -3,4 +3,5 @@ import { Value } from './value'
3
3
  export interface SymbolTable {
4
4
  lookup(sym: string): Value
5
5
  export(): Record<string, unknown>
6
+ exportValue(): Record<string, Value>
6
7
  }
@@ -11,4 +11,20 @@ describe('parser', () => {
11
11
  expect(() => parse(`{#$%x: 8}`)).toThrowError('Expected an identifier at (1:2..9) #$%x: 8}')
12
12
  expect(() => parse(`"foo" "goo"`)).toThrowError('Loitering input at (1:7..11) "goo"')
13
13
  })
14
+
15
+ describe('unit', () => {
16
+ test('show', () => {
17
+ expect(show(parse(`import * as foo from './bar';'a'`))).toEqual(`import * as foo from './bar';\n'a'`)
18
+ })
19
+ })
20
+ describe('expression', () => {
21
+ test('show', () => {
22
+ expect(show(parse(`'sunday'`))).toEqual(`'sunday'`)
23
+ expect(show(parse(`true`))).toEqual(`true`)
24
+ expect(show(parse(`500`))).toEqual(`500`)
25
+ expect(show(parse(`sink`))).toEqual(`sink`)
26
+ expect(show(parse(`sink!`))).toEqual(`sink!`)
27
+ expect(show(parse(`sink!!`))).toEqual(`sink!!`)
28
+ })
29
+ })
14
30
  })
@@ -0,0 +1,33 @@
1
+ import { Septima } from '../src/septima'
2
+ import { shouldNeverHappen } from '../src/should-never-happen'
3
+
4
+ /**
5
+ * Runs a Septima program for testing purposes. Throws an error If the program evaluated to `sink`.
6
+ */
7
+ async function run(mainModule: string, inputs: Record<string, string>) {
8
+ const septima = new Septima()
9
+ const res = septima.computeModule(mainModule, (m: string) => inputs[m])
10
+ if (res.tag === 'ok') {
11
+ return res.value
12
+ }
13
+
14
+ if (res.tag === 'sink') {
15
+ throw new Error(res.message)
16
+ }
17
+
18
+ shouldNeverHappen(res)
19
+ }
20
+
21
+ describe('septima-compute-module', () => {
22
+ test('fetches the content of the module to compute from the given callback function', async () => {
23
+ expect(await run('a', { a: `3+8` })).toEqual(11)
24
+ })
25
+ test('can use exported definitions from another module', async () => {
26
+ expect(await run('a', { a: `import * as b from 'b'; 3+b.eight`, b: `let eight = 8; {}` })).toEqual(11)
27
+ })
28
+ test('errors if the path to input from is not a string literal', async () => {
29
+ await expect(run('a', { a: `import * as foo from 500` })).rejects.toThrowError(
30
+ 'Expected a string literal at (1:22..24) 500',
31
+ )
32
+ })
33
+ })
@@ -15,8 +15,8 @@ function run(input: string) {
15
15
  * @param input the Septima program to run
16
16
  */
17
17
  function runSink(input: string) {
18
- const septima = new Septima(input)
19
- const res = septima.compute()
18
+ const septima = new Septima()
19
+ const res = septima.compute(input)
20
20
 
21
21
  if (res.tag !== 'sink') {
22
22
  throw new Error(`Not a sink: ${res.value}`)
@@ -84,6 +84,7 @@ describe('septima', () => {
84
84
  const expected = [
85
85
  `value type error: expected num but found "zxcvbnm" when evaluating:`,
86
86
  ` at (1:1..21) 9 * 8 * 'zxcvbnm' * 7`,
87
+ ` at (1:1..21) 9 * 8 * 'zxcvbnm' * 7`,
87
88
  ` at (1:5..21) 8 * 'zxcvbnm' * 7`,
88
89
  ` at (1:10..21) zxcvbnm' * 7`,
89
90
  ].join('\n')
@@ -612,6 +613,7 @@ describe('septima', () => {
612
613
  test(`captures the expression trace at runtime`, () => {
613
614
  expect(runSink(`1000 + 2000 + 3000 + sink!`).trace).toEqual(
614
615
  [
616
+ ` at (1:1..26) 1000 + 2000 + 3000 + sink!`,
615
617
  ` at (1:1..26) 1000 + 2000 + 3000 + sink!`,
616
618
  ` at (1:8..26) 2000 + 3000 + sink!`,
617
619
  ` at (1:15..26) 3000 + sink!`,
@@ -632,6 +634,7 @@ describe('septima', () => {
632
634
  expect(Object.keys(actual.symbols ?? {})).toEqual(['Object', 'a', 'f', 'x', 'y'])
633
635
  expect(actual.trace).toEqual(
634
636
  [
637
+ ` at (1:1..58) let a = 2; let f = fun(x, y) x * y * sink!! * a; f(30, 40)`,
635
638
  ` at (1:1..58) let a = 2; let f = fun(x, y) x * y * sink!! * a; f(30, 40)`,
636
639
  ` at (1:50..58) f(30, 40)`,
637
640
  ` at (1:30..47) x * y * sink!! * a`,
@@ -794,17 +797,23 @@ describe('septima', () => {
794
797
  })
795
798
  describe('preimport', () => {
796
799
  test('definitions from a preimported file can be used', () => {
797
- const septima = new Septima(`libA.plus10(4) + libA.plus20(2)`, {
800
+ const septima = new Septima()
801
+
802
+ const input = `libA.plus10(4) + libA.plus20(2)`
803
+ const preimports = {
798
804
  libA: `{ plus10: fun (n) n+10, plus20: fun (n) n+20}`,
799
- })
800
- expect(septima.compute()).toMatchObject({ value: 36 })
805
+ }
806
+ expect(septima.compute(input, preimports)).toMatchObject({ value: 36 })
801
807
  })
802
808
  test('supports multiple preimports', () => {
803
- const septima = new Septima(`a.calc(4) + b.calc(1)`, {
809
+ const septima = new Septima()
810
+
811
+ const input = `a.calc(4) + b.calc(1)`
812
+ const preimports = {
804
813
  a: `{ calc: fun (n) n+10 }`,
805
814
  b: `{ calc: fun (n) n+20 }`,
806
- })
807
- expect(septima.compute()).toMatchObject({ value: 35 })
815
+ }
816
+ expect(septima.compute(input, preimports)).toMatchObject({ value: 35 })
808
817
  })
809
818
  })
810
819
  test.todo('support file names in locations')
@@ -825,4 +834,6 @@ describe('septima', () => {
825
834
  test.todo('number methods')
826
835
  test.todo('drop the fun () notation and use just arrow functions')
827
836
  test.todo('proper internal representation of arrow function, in particular: show(), span()')
837
+ test.todo('sink sinkifies arrays and objects it is stored at')
838
+ test.todo('{foo}')
828
839
  })