septima-lang 0.0.2 → 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/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) {
@@ -22,14 +63,18 @@ export class Parser {
22
63
  const ident = this.identifier()
23
64
  this.scanner.consume('=')
24
65
  const value = this.lambda()
25
- this.scanner.consume(';')
26
-
27
66
  ret.push({ start, ident, value })
67
+
68
+ this.scanner.consumeIf(';')
69
+ if (!this.scanner.headMatches('let ')) {
70
+ return ret
71
+ }
28
72
  }
29
73
  }
30
74
 
31
75
  expression(): AstNode {
32
76
  const definitions = this.definitions()
77
+ this.scanner.consumeIf('return')
33
78
  const computation = this.lambda()
34
79
 
35
80
  if (definitions.length === 0) {
@@ -42,7 +87,7 @@ export class Parser {
42
87
  lambda(): AstNode {
43
88
  const start = this.scanner.consumeIf('fun')
44
89
  if (!start) {
45
- return this.ifExpression()
90
+ return this.arrowFunction()
46
91
  }
47
92
 
48
93
  this.scanner.consume('(')
@@ -66,6 +111,60 @@ export class Parser {
66
111
  return { tag: 'lambda', start, formalArgs: args, body }
67
112
  }
68
113
 
114
+ arrowFunction(): AstNode {
115
+ if (this.scanner.headMatches('(', ')', '=>')) {
116
+ const start = this.scanner.consume('(')
117
+ this.scanner.consume(')')
118
+ this.scanner.consume('=>')
119
+ const body = this.lambdaBody()
120
+ return { tag: 'lambda', start, formalArgs: [], body }
121
+ }
122
+ if (this.scanner.headMatches(IDENT_PATTERN, '=>')) {
123
+ const ident = this.identifier()
124
+ this.scanner.consume('=>')
125
+ const body = this.lambdaBody()
126
+ return { tag: 'lambda', start: ident.t, formalArgs: [ident], body }
127
+ }
128
+ if (this.scanner.headMatches('(', IDENT_PATTERN, ')', '=>')) {
129
+ const start = this.scanner.consume('(')
130
+ const ident = this.identifier()
131
+ this.scanner.consume(')')
132
+ this.scanner.consume('=>')
133
+ const body = this.lambdaBody()
134
+ return { tag: 'lambda', start, formalArgs: [ident], body }
135
+ }
136
+ if (this.scanner.headMatches('(', IDENT_PATTERN, ',')) {
137
+ const start = this.scanner.consume('(')
138
+ const formalArgs: Ident[] = []
139
+ while (true) {
140
+ const ident = this.identifier()
141
+ formalArgs.push(ident)
142
+
143
+ if (this.scanner.consumeIf(')')) {
144
+ break
145
+ }
146
+
147
+ this.scanner.consume(',')
148
+ }
149
+
150
+ this.scanner.consume('=>')
151
+ const body = this.lambdaBody()
152
+ return { tag: 'lambda', start, formalArgs, body }
153
+ }
154
+
155
+ return this.ifExpression()
156
+ }
157
+
158
+ private lambdaBody() {
159
+ if (this.scanner.consumeIf('{')) {
160
+ const ret = this.expression()
161
+ this.scanner.consume('}')
162
+ return ret
163
+ }
164
+
165
+ return this.expression()
166
+ }
167
+
69
168
  ifExpression(): AstNode {
70
169
  if (!this.scanner.consumeIf('if')) {
71
170
  return this.unsink()
@@ -252,6 +351,18 @@ export class Parser {
252
351
  }
253
352
 
254
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 {
255
366
  let t = this.scanner.consumeIf('sink!!')
256
367
  if (t) {
257
368
  return { tag: 'literal', type: 'sink!!', t }
@@ -295,7 +406,11 @@ export class Parser {
295
406
  return { tag: 'literal', type: 'str', t }
296
407
  }
297
408
 
298
- t = this.scanner.consumeIf('[')
409
+ return undefined
410
+ }
411
+
412
+ maybeCompositeLiteral(): AstNode | undefined {
413
+ let t = this.scanner.consumeIf('[')
299
414
  if (t) {
300
415
  return this.arrayBody(t)
301
416
  }
@@ -305,12 +420,7 @@ export class Parser {
305
420
  return this.objectBody(t)
306
421
  }
307
422
 
308
- const ident = this.maybeIdentifier()
309
- if (ident) {
310
- return ident
311
- }
312
-
313
- throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
423
+ return undefined
314
424
  }
315
425
 
316
426
  /**
@@ -326,6 +436,13 @@ export class Parser {
326
436
 
327
437
  const parts: ArrayLiteralPart[] = []
328
438
  while (true) {
439
+ if (this.scanner.consumeIf(',')) {
440
+ const end = this.scanner.consumeIf(']')
441
+ if (end) {
442
+ return { tag: 'arrayLiteral', start, parts, end }
443
+ }
444
+ continue
445
+ }
329
446
  if (this.scanner.consumeIf('...')) {
330
447
  parts.push({ tag: 'spread', v: this.expression() })
331
448
  } else {
@@ -333,12 +450,16 @@ export class Parser {
333
450
  parts.push({ tag: 'element', v: exp })
334
451
  }
335
452
 
336
- const end = this.scanner.consumeIf(']')
453
+ let end = this.scanner.consumeIf(']')
337
454
  if (end) {
338
455
  return { tag: 'arrayLiteral', start, parts, end }
339
456
  }
340
457
 
341
458
  this.scanner.consume(',')
459
+ end = this.scanner.consumeIf(']')
460
+ if (end) {
461
+ return { tag: 'arrayLiteral', start, parts, end }
462
+ }
342
463
  }
343
464
  }
344
465
 
@@ -370,12 +491,16 @@ export class Parser {
370
491
  parts.push({ tag: 'hardName', k, v })
371
492
  }
372
493
 
373
- const end = this.scanner.consumeIf('}')
494
+ let end = this.scanner.consumeIf('}')
374
495
  if (end) {
375
496
  return { tag: 'objectLiteral', start, parts, end }
376
497
  }
377
498
 
378
499
  this.scanner.consume(',')
500
+ end = this.scanner.consumeIf('}')
501
+ if (end) {
502
+ return { tag: 'objectLiteral', start, parts, end }
503
+ }
379
504
  }
380
505
  }
381
506
 
@@ -389,7 +514,7 @@ export class Parser {
389
514
  }
390
515
 
391
516
  private maybeIdentifier(): Ident | undefined {
392
- const t = this.scanner.consumeIf(/[a-zA-Z][0-9A-Za-z_]*/)
517
+ const t = this.scanner.consumeIf(IDENT_PATTERN)
393
518
  if (t) {
394
519
  return { tag: 'ident', t }
395
520
  }
@@ -397,3 +522,5 @@ export class Parser {
397
522
  return undefined
398
523
  }
399
524
  }
525
+
526
+ const IDENT_PATTERN = /[a-zA-Z][0-9A-Za-z_]*/
package/src/runtime.ts CHANGED
@@ -1,7 +1,6 @@
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
- import { Parser } from './parser'
5
4
  import { shouldNeverHappen } from './should-never-happen'
6
5
  import * as Stack from './stack'
7
6
  import { switchOn } from './switch-on'
@@ -32,6 +31,12 @@ class SymbolFrame implements SymbolTable {
32
31
  ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
33
32
  return ret
34
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
+ }
35
40
  }
36
41
 
37
42
  class EmptySymbolTable implements SymbolTable {
@@ -42,6 +47,10 @@ class EmptySymbolTable implements SymbolTable {
42
47
  export() {
43
48
  return {}
44
49
  }
50
+
51
+ exportValue(): Record<string, Value> {
52
+ return {}
53
+ }
45
54
  }
46
55
 
47
56
  export type Verbosity = 'quiet' | 'trace'
@@ -51,18 +60,27 @@ export class Runtime {
51
60
  constructor(
52
61
  private readonly root: AstNode,
53
62
  private readonly verbosity: Verbosity = 'quiet',
54
- private readonly parser: Parser,
63
+ private readonly preimports: Record<string, Value>,
64
+ private readonly getAstOf: (fileName: string) => Unit,
55
65
  ) {}
56
66
 
57
- compute() {
67
+ private buildInitialSymbolTable() {
58
68
  const empty = new EmptySymbolTable()
59
69
 
60
70
  const keys = Value.foreign(o => o.keys())
61
71
  const entries = Value.foreign(o => o.entries())
62
72
  const fromEntries = Value.foreign(o => o.fromEntries())
63
- const lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
73
+ let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
74
+
75
+ for (const [importName, importValue] of Object.entries(this.preimports)) {
76
+ lib = new SymbolFrame(importName, { destination: importValue }, lib)
77
+ }
78
+ return lib
79
+ }
80
+
81
+ compute() {
64
82
  try {
65
- const value = this.evalNode(this.root, lib)
83
+ const value = this.evalNode(this.root, this.buildInitialSymbolTable())
66
84
  return { value }
67
85
  } catch (e) {
68
86
  const trace: AstNode[] = []
@@ -95,7 +113,50 @@ export class Runtime {
95
113
  return ret
96
114
  }
97
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
+
98
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
+ }
99
160
  if (ast.tag === 'topLevelExpression') {
100
161
  let newTable = table
101
162
  for (const def of ast.definitions) {
@@ -109,6 +170,10 @@ export class Runtime {
109
170
  return this.evalNode(ast.computation, newTable)
110
171
  }
111
172
 
173
+ if (ast.tag === 'export*') {
174
+ return Value.obj(table.exportValue())
175
+ }
176
+
112
177
  if (ast.tag === 'binaryOperator') {
113
178
  const lhs = this.evalNode(ast.lhs, table)
114
179
  if (ast.operator === '||') {
package/src/scanner.ts CHANGED
@@ -7,10 +7,10 @@ export interface Token {
7
7
  }
8
8
 
9
9
  export class Scanner {
10
- private offset = 0
11
-
12
- constructor(private readonly sourceCode: SourceCode) {
13
- this.eatWhitespace()
10
+ constructor(readonly sourceCode: SourceCode, private offset = 0) {
11
+ if (this.offset === 0) {
12
+ this.eatWhitespace()
13
+ }
14
14
  }
15
15
 
16
16
  get sourceRef() {
@@ -52,6 +52,18 @@ export class Scanner {
52
52
  }
53
53
  }
54
54
 
55
+ headMatches(...patterns: (RegExp | string)[]): boolean {
56
+ const alt = new Scanner(this.sourceCode, this.offset)
57
+ for (const p of patterns) {
58
+ const t = alt.consumeIf(p, true)
59
+ if (t === undefined) {
60
+ return false
61
+ }
62
+ }
63
+
64
+ return true
65
+ }
66
+
55
67
  consume(r: RegExp | string, eatWhitespace = true): Token {
56
68
  const text = this.match(r)
57
69
  if (text === undefined) {
package/src/septima.ts CHANGED
@@ -1,9 +1,11 @@
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'
4
5
  import { Scanner } from './scanner'
5
6
  import { shouldNeverHappen } from './should-never-happen'
6
7
  import { SourceCode } from './source-code'
8
+ import { Value } from './value'
7
9
 
8
10
  interface Options {
9
11
  /**
@@ -15,10 +17,6 @@ interface Options {
15
17
  }
16
18
 
17
19
  export class Septima {
18
- private readonly scanner
19
- private readonly sourceCode
20
- private readonly parser
21
-
22
20
  /**
23
21
  * Runs a Septima program and returns the value it evaluates to. If it evaluates to `sink`, returns the value computed
24
22
  * by `options.onSink()` - if present, or throws an error - otherwise.
@@ -36,7 +34,7 @@ export class Septima {
36
34
  ((r: ResultSink) => {
37
35
  throw new Error(r.message)
38
36
  })
39
- const res = new Septima(input).compute()
37
+ const res = new Septima().compute(input)
40
38
  if (res.tag === 'ok') {
41
39
  return res.value
42
40
  }
@@ -48,30 +46,68 @@ export class Septima {
48
46
  shouldNeverHappen(res)
49
47
  }
50
48
 
51
- constructor(readonly input: string) {
52
- this.sourceCode = new SourceCode(this.input)
53
- this.scanner = new Scanner(this.sourceCode)
54
- this.parser = new Parser(this.scanner)
49
+ constructor() {}
50
+
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)
55
59
  }
56
60
 
57
- compute(verbosity: Verbosity = 'quiet'): Result {
58
- const ast = parse(this.parser)
59
- const runtime = new Runtime(ast, verbosity, this.parser)
61
+ compute(input: string, preimports: Record<string, string> = {}, verbosity: Verbosity = 'quiet'): Result {
62
+ const lib: Record<string, Value> = {}
63
+ for (const [importName, importCode] of Object.entries(preimports)) {
64
+ const sourceCode = new SourceCode(importCode)
65
+ const value = this.computeImpl(sourceCode, verbosity, {}, undefined)
66
+ if (value.isSink()) {
67
+ // TODO(imaman): cover!
68
+ const r = new ResultSinkImpl(value, sourceCode)
69
+ throw new Error(`preimport (${importName}) evaluated to sink: ${r.message}`)
70
+ }
71
+ lib[importName] = value
72
+ }
73
+
74
+ const sourceCode = new SourceCode(input)
75
+ const value = this.computeImpl(sourceCode, verbosity, lib, undefined)
76
+ if (!value.isSink()) {
77
+ return { value: value.export(), tag: 'ok' }
78
+ }
79
+ return new ResultSinkImpl(value, sourceCode)
80
+ }
81
+
82
+ private computeImpl(
83
+ sourceCode: SourceCode,
84
+ verbosity: Verbosity,
85
+ lib: Record<string, Value>,
86
+ moduleReader: undefined | ((m: string) => string),
87
+ ) {
88
+ const scanner = new Scanner(sourceCode)
89
+ const parser = new Parser(scanner)
90
+ const ast = parse(parser)
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)
60
99
  const c = runtime.compute()
61
100
 
62
101
  if (c.value) {
63
- if (!c.value.isSink()) {
64
- return { value: c.value.export(), tag: 'ok' }
65
- }
66
- return new ResultSinkImpl(c.value, this.sourceCode)
102
+ return c.value
67
103
  }
68
104
 
69
- const runtimeErrorMessage = `${c.errorMessage} when evaluating:\n${this.sourceCode.formatTrace(c.expressionTrace)}`
105
+ const runtimeErrorMessage = `${c.errorMessage} when evaluating:\n${sourceCode.formatTrace(c.expressionTrace)}`
70
106
  throw new Error(runtimeErrorMessage)
71
107
  }
72
108
  }
73
109
 
74
- export function parse(arg: string | Parser) {
110
+ export function parse(arg: string | Parser): Unit {
75
111
  const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
76
112
  const ast = parser.parse()
77
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
+ })