septima-lang 0.0.3 → 0.0.6

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) {
@@ -266,11 +307,15 @@ export class Parser {
266
307
  while (true) {
267
308
  const arg = this.expression()
268
309
  actualArgs.push(arg)
269
- const end = this.scanner.consumeIf(')')
310
+ let end = this.scanner.consumeIf(')')
270
311
  if (end) {
271
312
  return { actualArgs, end }
272
313
  }
273
314
  this.scanner.consume(',')
315
+ end = this.scanner.consumeIf(')')
316
+ if (end) {
317
+ return { actualArgs, end }
318
+ }
274
319
  }
275
320
  }
276
321
 
@@ -310,17 +355,29 @@ export class Parser {
310
355
  }
311
356
 
312
357
  literalOrIdent(): AstNode {
313
- let t = this.scanner.consumeIf('sink!!')
358
+ const ret = this.maybeLiteral() ?? this.maybeIdentifier()
359
+ if (!ret) {
360
+ throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
361
+ }
362
+ return ret
363
+ }
364
+
365
+ maybeLiteral(): AstNode | undefined {
366
+ return this.maybePrimitiveLiteral() ?? this.maybeCompositeLiteral()
367
+ }
368
+
369
+ maybePrimitiveLiteral(): Literal | undefined {
370
+ let t = this.scanner.consumeIf('sink!!') || this.scanner.consumeIf('undefined!!')
314
371
  if (t) {
315
372
  return { tag: 'literal', type: 'sink!!', t }
316
373
  }
317
374
 
318
- t = this.scanner.consumeIf('sink!')
375
+ t = this.scanner.consumeIf('sink!') || this.scanner.consumeIf('undefined!')
319
376
  if (t) {
320
377
  return { tag: 'literal', type: 'sink!', t }
321
378
  }
322
379
 
323
- t = this.scanner.consumeIf('sink')
380
+ t = this.scanner.consumeIf('sink') || this.scanner.consumeIf('undefined')
324
381
  if (t) {
325
382
  return { tag: 'literal', type: 'sink', t }
326
383
  }
@@ -353,7 +410,11 @@ export class Parser {
353
410
  return { tag: 'literal', type: 'str', t }
354
411
  }
355
412
 
356
- t = this.scanner.consumeIf('[')
413
+ return undefined
414
+ }
415
+
416
+ maybeCompositeLiteral(): AstNode | undefined {
417
+ let t = this.scanner.consumeIf('[')
357
418
  if (t) {
358
419
  return this.arrayBody(t)
359
420
  }
@@ -363,12 +424,7 @@ export class Parser {
363
424
  return this.objectBody(t)
364
425
  }
365
426
 
366
- const ident = this.maybeIdentifier()
367
- if (ident) {
368
- return ident
369
- }
370
-
371
- throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
427
+ return undefined
372
428
  }
373
429
 
374
430
  /**
@@ -384,6 +440,13 @@ export class Parser {
384
440
 
385
441
  const parts: ArrayLiteralPart[] = []
386
442
  while (true) {
443
+ if (this.scanner.consumeIf(',')) {
444
+ const end = this.scanner.consumeIf(']')
445
+ if (end) {
446
+ return { tag: 'arrayLiteral', start, parts, end }
447
+ }
448
+ continue
449
+ }
387
450
  if (this.scanner.consumeIf('...')) {
388
451
  parts.push({ tag: 'spread', v: this.expression() })
389
452
  } else {
@@ -391,12 +454,16 @@ export class Parser {
391
454
  parts.push({ tag: 'element', v: exp })
392
455
  }
393
456
 
394
- const end = this.scanner.consumeIf(']')
457
+ let end = this.scanner.consumeIf(']')
395
458
  if (end) {
396
459
  return { tag: 'arrayLiteral', start, parts, end }
397
460
  }
398
461
 
399
462
  this.scanner.consume(',')
463
+ end = this.scanner.consumeIf(']')
464
+ if (end) {
465
+ return { tag: 'arrayLiteral', start, parts, end }
466
+ }
400
467
  }
401
468
  }
402
469
 
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,12 @@ 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,
65
+ private readonly args: Record<string, unknown>,
54
66
  ) {}
55
67
 
56
- compute() {
68
+ private buildInitialSymbolTable(generateTheArgsObject: boolean) {
57
69
  const empty = new EmptySymbolTable()
58
70
 
59
71
  const keys = Value.foreign(o => o.keys())
@@ -61,12 +73,19 @@ export class Runtime {
61
73
  const fromEntries = Value.foreign(o => o.fromEntries())
62
74
  let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
63
75
 
76
+ if (generateTheArgsObject) {
77
+ lib = new SymbolFrame('args', { destination: Value.from(this.args) }, lib)
78
+ }
79
+
64
80
  for (const [importName, importValue] of Object.entries(this.preimports)) {
65
81
  lib = new SymbolFrame(importName, { destination: importValue }, lib)
66
82
  }
83
+ return lib
84
+ }
67
85
 
86
+ compute() {
68
87
  try {
69
- const value = this.evalNode(this.root, lib)
88
+ const value = this.evalNode(this.root, this.buildInitialSymbolTable(true))
70
89
  return { value }
71
90
  } catch (e) {
72
91
  const trace: AstNode[] = []
@@ -99,7 +118,50 @@ export class Runtime {
99
118
  return ret
100
119
  }
101
120
 
121
+ private importDefinitions(pathToImportFrom: string): Value {
122
+ const ast = this.getAstOf(pathToImportFrom)
123
+ const exp = ast.expression
124
+ const imports = ast.imports
125
+ if (
126
+ exp.tag === 'arrayLiteral' ||
127
+ exp.tag === 'binaryOperator' ||
128
+ exp.tag === 'dot' ||
129
+ exp.tag === 'export*' ||
130
+ exp.tag === 'functionCall' ||
131
+ exp.tag === 'ident' ||
132
+ exp.tag === 'if' ||
133
+ exp.tag === 'indexAccess' ||
134
+ exp.tag === 'lambda' ||
135
+ exp.tag === 'literal' ||
136
+ exp.tag === 'objectLiteral' ||
137
+ exp.tag === 'unaryOperator' ||
138
+ exp.tag === 'unit'
139
+ ) {
140
+ // TODO(imaman): throw an error on non-exporting unit?
141
+ return Value.obj({})
142
+ }
143
+
144
+ if (exp.tag === 'topLevelExpression') {
145
+ const unit: AstNode = {
146
+ tag: 'unit',
147
+ imports,
148
+ expression: { tag: 'topLevelExpression', definitions: exp.definitions, computation: { tag: 'export*' } },
149
+ }
150
+ return this.evalNode(unit, this.buildInitialSymbolTable(false))
151
+ }
152
+
153
+ shouldNeverHappen(exp)
154
+ }
155
+
102
156
  private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
157
+ if (ast.tag === 'unit') {
158
+ let newTable = table
159
+ for (const imp of ast.imports) {
160
+ const o = this.importDefinitions(imp.pathToImportFrom.text)
161
+ newTable = new SymbolFrame(imp.ident.t.text, { destination: o }, newTable)
162
+ }
163
+ return this.evalNode(ast.expression, newTable)
164
+ }
103
165
  if (ast.tag === 'topLevelExpression') {
104
166
  let newTable = table
105
167
  for (const def of ast.definitions) {
@@ -113,6 +175,10 @@ export class Runtime {
113
175
  return this.evalNode(ast.computation, newTable)
114
176
  }
115
177
 
178
+ if (ast.tag === 'export*') {
179
+ return Value.obj(table.exportValue())
180
+ }
181
+
116
182
  if (ast.tag === 'binaryOperator') {
117
183
  const lhs = this.evalNode(ast.lhs, table)
118
184
  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'
@@ -27,13 +28,13 @@ export class Septima {
27
28
  * @param options
28
29
  * @returns the value that `input` evaluates to
29
30
  */
30
- static run(input: string, options?: Options): unknown {
31
+ static run(input: string, options?: Options, args: Record<string, unknown> = {}): unknown {
31
32
  const onSink =
32
33
  options?.onSink ??
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, {}, 'quiet', args)
37
38
  if (res.tag === 'ok') {
38
39
  return res.value
39
40
  }
@@ -45,13 +46,28 @@ 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, args: Record<string, unknown>): Result {
52
+ const input = moduleReader(moduleName)
53
+ const sourceCode = new SourceCode(input)
54
+ const value = this.computeImpl(sourceCode, 'quiet', {}, moduleReader, args)
55
+ if (!value.isSink()) {
56
+ return { value: value.export(), tag: 'ok' }
57
+ }
58
+ return new ResultSinkImpl(value, sourceCode)
59
+ }
60
+
61
+ compute(
62
+ input: string,
63
+ preimports: Record<string, string> = {},
64
+ verbosity: Verbosity = 'quiet',
65
+ args: Record<string, unknown>,
66
+ ): Result {
51
67
  const lib: Record<string, Value> = {}
52
- for (const [importName, importCode] of Object.entries(this.preimports)) {
68
+ for (const [importName, importCode] of Object.entries(preimports)) {
53
69
  const sourceCode = new SourceCode(importCode)
54
- const value = this.computeImpl(sourceCode, verbosity, {})
70
+ const value = this.computeImpl(sourceCode, verbosity, {}, undefined, {})
55
71
  if (value.isSink()) {
56
72
  // TODO(imaman): cover!
57
73
  const r = new ResultSinkImpl(value, sourceCode)
@@ -60,20 +76,33 @@ export class Septima {
60
76
  lib[importName] = value
61
77
  }
62
78
 
63
- const sourceCode = new SourceCode(this.input)
64
- const value = this.computeImpl(sourceCode, verbosity, lib)
79
+ const sourceCode = new SourceCode(input)
80
+ const value = this.computeImpl(sourceCode, verbosity, lib, undefined, args)
65
81
  if (!value.isSink()) {
66
82
  return { value: value.export(), tag: 'ok' }
67
83
  }
68
84
  return new ResultSinkImpl(value, sourceCode)
69
85
  }
70
86
 
71
- private computeImpl(sourceCode: SourceCode, verbosity: Verbosity, lib: Record<string, Value>) {
87
+ private computeImpl(
88
+ sourceCode: SourceCode,
89
+ verbosity: Verbosity,
90
+ lib: Record<string, Value>,
91
+ moduleReader: undefined | ((m: string) => string),
92
+ args: Record<string, unknown>,
93
+ ) {
72
94
  const scanner = new Scanner(sourceCode)
73
95
  const parser = new Parser(scanner)
74
-
75
96
  const ast = parse(parser)
76
- const runtime = new Runtime(ast, verbosity, lib)
97
+
98
+ const getAstOf = (fileName: string) => {
99
+ if (!moduleReader) {
100
+ throw new Error(`cannot read modules`)
101
+ }
102
+ return parse(moduleReader(fileName))
103
+ }
104
+
105
+ const runtime = new Runtime(ast, verbosity, lib, getAstOf, args)
77
106
  const c = runtime.compute()
78
107
 
79
108
  if (c.value) {
@@ -85,7 +114,7 @@ export class Septima {
85
114
  }
86
115
  }
87
116
 
88
- export function parse(arg: string | Parser) {
117
+ export function parse(arg: string | Parser): Unit {
89
118
  const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
90
119
  const ast = parser.parse()
91
120
  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,41 @@
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>, args: Record<string, unknown> = {}) {
8
+ const septima = new Septima()
9
+ const res = septima.computeModule(mainModule, (m: string) => inputs[m], args)
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
+ test('support the passing of args into the runtime', async () => {
34
+ expect(await run('a', { a: `args.x * args.y` }, { x: 5, y: 9 })).toEqual(45)
35
+ })
36
+ test('the args object is available only at the main module', async () => {
37
+ await expect(
38
+ run('a', { a: `import * as b from 'b'; args.x + '_' + b.foo`, b: `let foo = args.x ?? 'N/A'; {}` }, { x: 'Red' }),
39
+ ).rejects.toThrowError(/Symbol args was not found when evaluating/)
40
+ })
41
+ })