septima-lang 0.1.0 → 0.2.0

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.
@@ -0,0 +1,340 @@
1
+ import { Span } from './location'
2
+ import { Token } from './scanner'
3
+ import { shouldNeverHappen } from './should-never-happen'
4
+ import { switchOn } from './switch-on'
5
+
6
+ export type Let = { start: Token; ident: Ident; value: AstNode; isExported: boolean }
7
+
8
+ export type UnitId = string
9
+
10
+ export type Import = {
11
+ start: Token
12
+ ident: Ident
13
+ pathToImportFrom: Token
14
+ unitId: UnitId
15
+ }
16
+
17
+ export type Literal = {
18
+ tag: 'literal'
19
+ type: 'str' | 'bool' | 'num' | 'undef'
20
+ t: Token
21
+ unitId: UnitId
22
+ }
23
+
24
+ export type ObjectLiteralPart =
25
+ | { tag: 'hardName'; k: Ident; v: AstNode }
26
+ | { tag: 'quotedString'; k: Literal; v: AstNode }
27
+ | { tag: 'computedName'; k: AstNode; v: AstNode }
28
+ | { tag: 'spread'; o: AstNode }
29
+
30
+ export type ArrayLiteralPart = { tag: 'element'; v: AstNode } | { tag: 'spread'; v: AstNode }
31
+
32
+ export type TemplatePart = { tag: 'string'; value: string } | { tag: 'expression'; expr: AstNode }
33
+
34
+ export type Ident = {
35
+ tag: 'ident'
36
+ t: Token
37
+ unitId: UnitId
38
+ }
39
+
40
+ export type FormalArg = {
41
+ tag: 'formalArg'
42
+ ident: Ident
43
+ defaultValue?: AstNode
44
+ unitId: UnitId
45
+ }
46
+
47
+ export type Lambda = {
48
+ tag: 'lambda'
49
+ start: Token
50
+ formalArgs: FormalArg[]
51
+ body: AstNode
52
+ unitId: UnitId
53
+ }
54
+
55
+ export type Unit = {
56
+ tag: 'unit'
57
+ imports: Import[]
58
+ expression: AstNode
59
+ unitId: UnitId
60
+ }
61
+
62
+ export type AstNode =
63
+ | Ident
64
+ | FormalArg
65
+ | Literal
66
+ | Unit
67
+ | {
68
+ start: Token
69
+ tag: 'arrayLiteral'
70
+ parts: ArrayLiteralPart[]
71
+ end: Token
72
+ unitId: UnitId
73
+ }
74
+ | {
75
+ start: Token
76
+ tag: 'objectLiteral'
77
+ parts: ObjectLiteralPart[]
78
+ end: Token
79
+ unitId: UnitId
80
+ }
81
+ | {
82
+ tag: 'binaryOperator'
83
+ operator: '+' | '-' | '*' | '/' | '**' | '%' | '&&' | '||' | '>' | '<' | '>=' | '<=' | '==' | '!=' | '??'
84
+ lhs: AstNode
85
+ rhs: AstNode
86
+ unitId: UnitId
87
+ }
88
+ | {
89
+ tag: 'unaryOperator'
90
+ operatorToken: Token
91
+ operator: '+' | '-' | '!'
92
+ operand: AstNode
93
+ unitId: UnitId
94
+ }
95
+ | {
96
+ tag: 'topLevelExpression'
97
+ definitions: Let[]
98
+ throwToken?: Token
99
+ computation?: AstNode
100
+ unitId: UnitId
101
+ }
102
+ | Lambda
103
+ | {
104
+ tag: 'ternary'
105
+ condition: AstNode
106
+ positive: AstNode
107
+ negative: AstNode
108
+ unitId: UnitId
109
+ }
110
+ | {
111
+ tag: 'functionCall'
112
+ actualArgs: AstNode[]
113
+ callee: AstNode
114
+ end: Token
115
+ unitId: UnitId
116
+ }
117
+ | {
118
+ tag: 'if'
119
+ condition: AstNode
120
+ positive: AstNode
121
+ negative: AstNode
122
+ unitId: UnitId
123
+ }
124
+ | {
125
+ tag: 'dot'
126
+ receiver: AstNode
127
+ ident: Ident
128
+ unitId: UnitId
129
+ }
130
+ | {
131
+ tag: 'indexAccess'
132
+ receiver: AstNode
133
+ index: AstNode
134
+ unitId: UnitId
135
+ }
136
+ | {
137
+ // A special AST node meant to be generated internally (needed for exporting definition from one unit to another).
138
+ // Not intended to be parsed from source code. Hence, it is effectively empty, and its location cannot be
139
+ // determined.
140
+ tag: 'export*'
141
+ unitId: UnitId
142
+ }
143
+ | {
144
+ tag: 'templateLiteral'
145
+ parts: TemplatePart[]
146
+ start: Token
147
+ end: Token
148
+ unitId: UnitId
149
+ }
150
+
151
+ export function show(ast: AstNode | AstNode[]): string {
152
+ if (Array.isArray(ast)) {
153
+ return ast.map(curr => show(curr)).join(', ')
154
+ }
155
+
156
+ if (ast.tag === 'arrayLiteral') {
157
+ const parts = ast.parts.map(p => {
158
+ if (p.tag === 'element') {
159
+ return show(p.v)
160
+ }
161
+
162
+ if (p.tag === 'spread') {
163
+ return `...${show(p.v)}`
164
+ }
165
+
166
+ shouldNeverHappen(p)
167
+ })
168
+ return `[${parts.join(', ')}]`
169
+ }
170
+
171
+ if (ast.tag === 'binaryOperator') {
172
+ return `(${show(ast.lhs)} ${ast.operator} ${show(ast.rhs)})`
173
+ }
174
+ if (ast.tag === 'dot') {
175
+ return `${show(ast.receiver)}.${show(ast.ident)}`
176
+ }
177
+ if (ast.tag === 'export*') {
178
+ return `(export*)`
179
+ }
180
+ if (ast.tag === 'ternary') {
181
+ return `${show(ast.condition)} ? ${show(ast.positive)} : ${show(ast.negative)}`
182
+ }
183
+ if (ast.tag === 'functionCall') {
184
+ return `${show(ast.callee)}(${show(ast.actualArgs)})`
185
+ }
186
+ if (ast.tag === 'ident') {
187
+ return ast.t.text
188
+ }
189
+ if (ast.tag === 'formalArg') {
190
+ return ast.defaultValue ? `${show(ast.ident)} = ${show(ast.defaultValue)}` : show(ast.ident)
191
+ }
192
+ if (ast.tag === 'if') {
193
+ return `if (${show(ast.condition)}) ${show(ast.positive)} else ${show(ast.negative)}`
194
+ }
195
+ if (ast.tag === 'indexAccess') {
196
+ return `${show(ast.receiver)}[${show(ast.index)}]`
197
+ }
198
+ if (ast.tag === 'lambda') {
199
+ return `fun (${show(ast.formalArgs)}) ${show(ast.body)}`
200
+ }
201
+ if (ast.tag === 'literal') {
202
+ return switchOn(ast.type, {
203
+ bool: () => ast.t.text,
204
+ num: () => ast.t.text,
205
+ undef: () => 'undefined',
206
+ str: () => `'${ast.t.text}'`,
207
+ })
208
+ }
209
+ if (ast.tag === 'templateLiteral') {
210
+ const partsStr = ast.parts
211
+ .map(p => {
212
+ if (p.tag === 'string') {
213
+ return p.value
214
+ }
215
+ if (p.tag === 'expression') {
216
+ return `\${${show(p.expr)}}`
217
+ }
218
+ shouldNeverHappen(p)
219
+ })
220
+ .join('')
221
+ return `\`${partsStr}\``
222
+ }
223
+ if (ast.tag === 'objectLiteral') {
224
+ const pairs = ast.parts.map(p => {
225
+ if (p.tag === 'computedName') {
226
+ return `[${show(p.k)}]: ${show(p.v)}`
227
+ }
228
+
229
+ if (p.tag === 'hardName') {
230
+ return `${show(p.k)}: ${show(p.v)}`
231
+ }
232
+
233
+ if (p.tag === 'quotedString') {
234
+ return `${show(p.k)}: ${show(p.v)}`
235
+ }
236
+
237
+ if (p.tag === 'spread') {
238
+ return `...${show(p.o)}`
239
+ }
240
+
241
+ shouldNeverHappen(p)
242
+ })
243
+ return `{${pairs.join(', ')}}`
244
+ }
245
+ if (ast.tag === 'topLevelExpression') {
246
+ const defs = ast.definitions
247
+ .map(d => `${d.isExported ? 'export ' : ''}let ${show(d.ident)} = ${show(d.value)}`)
248
+ .join('; ')
249
+ const sep = defs && ast.computation ? ' ' : ''
250
+ return `${defs ? defs + ';' : ''}${sep}${ast.throwToken ? ast.throwToken.text + ' ' : ''}${
251
+ ast.computation ? show(ast.computation) : ''
252
+ }`
253
+ }
254
+ if (ast.tag === 'unaryOperator') {
255
+ return `${ast.operator}${show(ast.operand)}`
256
+ }
257
+ if (ast.tag === 'unit') {
258
+ const imports = ast.imports
259
+ .map(imp => `import * as ${show(imp.ident)} from '${imp.pathToImportFrom.text}';`)
260
+ .join('\n')
261
+ return `${imports ? imports + '\n' : ''}${show(ast.expression)}`
262
+ }
263
+
264
+ shouldNeverHappen(ast)
265
+ }
266
+
267
+ export function span(ast: AstNode): Span {
268
+ const ofRange = (a: Span, b: Span) => ({ from: a.from, to: b.to })
269
+ const ofToken = (t: Token) => ({ from: t.location, to: { offset: t.location.offset + t.text.length - 1 } })
270
+
271
+ if (ast.tag === 'arrayLiteral') {
272
+ return ofRange(ofToken(ast.start), ofToken(ast.end))
273
+ }
274
+ if (ast.tag === 'binaryOperator') {
275
+ return ofRange(span(ast.lhs), span(ast.rhs))
276
+ }
277
+ if (ast.tag === 'dot') {
278
+ return ofRange(span(ast.receiver), span(ast.ident))
279
+ }
280
+ if (ast.tag === 'functionCall') {
281
+ return ofRange(span(ast.callee), ofToken(ast.end))
282
+ }
283
+ if (ast.tag === 'ident') {
284
+ return ofToken(ast.t)
285
+ }
286
+ if (ast.tag === 'formalArg') {
287
+ if (ast.defaultValue) {
288
+ return ofRange(span(ast.ident), span(ast.defaultValue))
289
+ }
290
+ return span(ast.ident)
291
+ }
292
+
293
+ if (ast.tag === 'export*') {
294
+ return { from: { offset: 0 }, to: { offset: 0 } }
295
+ }
296
+ if (ast.tag === 'if') {
297
+ return ofRange(span(ast.condition), span(ast.negative))
298
+ }
299
+ if (ast.tag === 'indexAccess') {
300
+ return ofRange(span(ast.receiver), span(ast.index))
301
+ }
302
+ if (ast.tag === 'lambda') {
303
+ return ofRange(ofToken(ast.start), span(ast.body))
304
+ }
305
+ if (ast.tag === 'ternary') {
306
+ return ofRange(span(ast.condition), span(ast.negative))
307
+ }
308
+ if (ast.tag === 'literal') {
309
+ return ofToken(ast.t)
310
+ }
311
+ if (ast.tag === 'templateLiteral') {
312
+ return ofRange(ofToken(ast.start), ofToken(ast.end))
313
+ }
314
+ if (ast.tag === 'objectLiteral') {
315
+ return ofRange(ofToken(ast.start), ofToken(ast.end))
316
+ }
317
+ if (ast.tag === 'topLevelExpression') {
318
+ if (ast.computation) {
319
+ const d0 = ast.definitions.find(Boolean)
320
+ const comp = span(ast.computation)
321
+ return ofRange(d0 ? ofToken(d0.start) : comp, comp)
322
+ } else if (ast.definitions.length) {
323
+ const first = ast.definitions[0]
324
+ const last = ast.definitions[ast.definitions.length - 1]
325
+ return ofRange(ofToken(first.start), span(last.value))
326
+ } else {
327
+ return { from: { offset: 0 }, to: { offset: 0 } }
328
+ }
329
+ }
330
+ if (ast.tag === 'unaryOperator') {
331
+ return ofRange(ofToken(ast.operatorToken), span(ast.operand))
332
+ }
333
+ if (ast.tag === 'unit') {
334
+ const i0 = ast.imports.find(Boolean)
335
+ const exp = span(ast.expression)
336
+ return ofRange(i0 ? ofToken(i0.start) : exp, exp)
337
+ }
338
+
339
+ shouldNeverHappen(ast)
340
+ }
@@ -0,0 +1,5 @@
1
+ export function extractMessage(e: unknown): string | unknown {
2
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
3
+ const typed = e as { message?: string }
4
+ return typed.message
5
+ }
package/src/fail-me.ts ADDED
@@ -0,0 +1,7 @@
1
+ export function failMe<R>(hint?: string): NonNullable<R> {
2
+ if (!hint) {
3
+ throw new Error(`This expression must never be evaluated`)
4
+ }
5
+
6
+ throw new Error(`Bad value: ${hint}`)
7
+ }
@@ -0,0 +1,124 @@
1
+ import { Value } from './value'
2
+
3
+ export type CallEvaluator = (callable: Value, args: Value[]) => Value
4
+ /**
5
+ * return an implementation for array methods. The following Array methods were not implemented
6
+ * as it is hard to find an intuitive immutable API due to their mutable nature:
7
+ * - copyWithin
8
+ * - fill
9
+ * - forEach
10
+ * - keys
11
+ * - pop
12
+ * - push
13
+ * - shift
14
+ * - unshift
15
+ */
16
+ export function findArrayMethod(arr: unknown[], index: string, callEvaluator: CallEvaluator) {
17
+ const adjustedCallback =
18
+ (callback: Value) =>
19
+ (...args: unknown[]) =>
20
+ callEvaluator(
21
+ callback,
22
+ args.map(x => Value.from(x)),
23
+ )
24
+
25
+ const adjustedPredicate =
26
+ (predicate: Value) =>
27
+ (...args: unknown[]) =>
28
+ callEvaluator(
29
+ predicate,
30
+ args.map(x => Value.from(x)),
31
+ ).assertBool()
32
+
33
+ if (index === 'at') {
34
+ return Value.foreign(n => arr.at(n.assertNum()))
35
+ }
36
+ if (index === 'concat') {
37
+ return Value.foreign(arg => arr.concat(arg.assertArr()))
38
+ }
39
+ if (index === 'entries') {
40
+ return Value.foreign(() => [...arr.entries()])
41
+ }
42
+ if (index === 'every') {
43
+ return Value.foreign(predicate => arr.every(adjustedPredicate(predicate)))
44
+ }
45
+ if (index === 'filter') {
46
+ return Value.foreign(predicate => arr.filter(adjustedPredicate(predicate)))
47
+ }
48
+ if (index === 'find') {
49
+ return Value.foreign(predicate => arr.find(adjustedPredicate(predicate)))
50
+ }
51
+ if (index === 'findIndex') {
52
+ return Value.foreign(predicate => arr.findIndex(adjustedPredicate(predicate)))
53
+ }
54
+ if (index === 'flatMap') {
55
+ return Value.foreign(callback => flatten(arr.map(adjustedCallback(callback))))
56
+ }
57
+ if (index === 'flat') {
58
+ return Value.foreign(() => flatten(arr))
59
+ }
60
+ if (index === 'includes') {
61
+ return Value.foreign((arg: Value) => arr.some(curr => Value.from(curr).equalsTo(arg).isTrue()))
62
+ }
63
+ if (index === 'indexOf') {
64
+ return Value.foreign(arg => arr.findIndex(curr => Value.from(curr).equalsTo(arg).isTrue()))
65
+ }
66
+ if (index === 'join') {
67
+ return Value.foreign(arg => arr.join(arg.assertStr()))
68
+ }
69
+ if (index === 'lastIndexOf') {
70
+ return Value.foreign(arg => {
71
+ for (let i = arr.length - 1; i >= 0; --i) {
72
+ if (Value.from(arr[i]).equalsTo(arg).isTrue()) {
73
+ return i
74
+ }
75
+ }
76
+ return -1
77
+ })
78
+ }
79
+ if (index === 'length') {
80
+ return Value.num(arr.length)
81
+ }
82
+ if (index === 'map') {
83
+ return Value.foreign(callback => arr.map(adjustedCallback(callback)))
84
+ }
85
+ if (index === 'reverse') {
86
+ return Value.foreign(() => [...arr].reverse())
87
+ }
88
+ if (index === 'reduce') {
89
+ return Value.foreign((callback, initialValue) => arr.reduce(adjustedCallback(callback), initialValue))
90
+ }
91
+ if (index === 'reduceRight') {
92
+ return Value.foreign((callback, initialValue) => arr.reduceRight(adjustedCallback(callback), initialValue))
93
+ }
94
+ if (index === 'slice') {
95
+ return Value.foreign((start, end) => arr.slice(start?.assertNum(), end?.assertNum()))
96
+ }
97
+ if (index === 'some') {
98
+ return Value.foreign(predicate => arr.some(adjustedPredicate(predicate)))
99
+ }
100
+ if (index === 'sort') {
101
+ return Value.foreign(callback => {
102
+ if (callback) {
103
+ return [...arr].sort((a, b) => callEvaluator(callback, [Value.from(a), Value.from(b)]).assertNum())
104
+ } else {
105
+ return [...arr].sort()
106
+ }
107
+ })
108
+ }
109
+ throw new Error(`Unrecognized array method: ${index}`)
110
+ }
111
+
112
+ function flatten(input: unknown[]) {
113
+ const ret = []
114
+ for (const curr of input) {
115
+ const v = Value.from(curr)
116
+ const unwrapped = v.unwrap()
117
+ if (Array.isArray(unwrapped)) {
118
+ ret.push(...unwrapped)
119
+ } else {
120
+ ret.push(v)
121
+ }
122
+ }
123
+ return ret
124
+ }
@@ -0,0 +1,84 @@
1
+ import { Value } from './value'
2
+
3
+ export function findStringMethod(s: string, indexValue: string | Value) {
4
+ const index = Value.toStringOrNumber(indexValue)
5
+ if (typeof index === 'number') {
6
+ throw new Error(`Index is of type number - not supported`)
7
+ }
8
+ if (index === 'at') {
9
+ return Value.foreign(n => s.at(n.assertNum()))
10
+ }
11
+ if (index === 'charAt') {
12
+ return Value.foreign(n => s.charAt(n.assertNum()))
13
+ }
14
+ if (index === 'concat') {
15
+ return Value.foreign(arg => s.concat(arg.assertStr()))
16
+ }
17
+ if (index === 'endsWith') {
18
+ return Value.foreign(arg => s.endsWith(arg.assertStr()))
19
+ }
20
+ if (index === 'includes') {
21
+ return Value.foreign(arg => s.includes(arg.assertStr()))
22
+ }
23
+ if (index === 'indexOf') {
24
+ return Value.foreign(searchString => s.indexOf(searchString.assertStr()))
25
+ }
26
+ if (index === 'lastIndexOf') {
27
+ return Value.foreign(searchString => s.lastIndexOf(searchString.assertStr()))
28
+ }
29
+ if (index === 'length') {
30
+ return Value.num(s.length)
31
+ }
32
+ if (index === 'match') {
33
+ return Value.foreign(r => s.match(r.assertStr()))
34
+ }
35
+ if (index === 'matchAll') {
36
+ return Value.foreign(r => [...s.matchAll(new RegExp(r.assertStr(), 'g'))])
37
+ }
38
+ if (index === 'padEnd') {
39
+ return Value.foreign((maxLength, fillString) => s.padEnd(maxLength.assertNum(), fillString?.assertStr()))
40
+ }
41
+ if (index === 'padStart') {
42
+ return Value.foreign((maxLength, fillString) => s.padStart(maxLength.assertNum(), fillString?.assertStr()))
43
+ }
44
+ if (index === 'repeat') {
45
+ return Value.foreign(count => s.repeat(count.assertNum()))
46
+ }
47
+ if (index === 'replace') {
48
+ return Value.foreign((searchValue, replacer) => s.replace(searchValue.assertStr(), replacer.assertStr()))
49
+ }
50
+ if (index === 'replaceAll') {
51
+ return Value.foreign((searchValue, replacer) => s.replaceAll(searchValue.assertStr(), replacer.assertStr()))
52
+ }
53
+ if (index === 'search') {
54
+ return Value.foreign(searcher => s.search(searcher.assertStr()))
55
+ }
56
+ if (index === 'slice') {
57
+ return Value.foreign((start, end) => s.slice(start?.assertNum(), end?.assertNum()))
58
+ }
59
+ if (index === 'split') {
60
+ return Value.foreign(splitter => s.split(splitter.assertStr()))
61
+ }
62
+ if (index === 'startsWith') {
63
+ return Value.foreign(arg => s.startsWith(arg.assertStr()))
64
+ }
65
+ if (index === 'substring') {
66
+ return Value.foreign((start, end) => s.substring(start.assertNum(), end?.assertNum()))
67
+ }
68
+ if (index === 'toLowerCase') {
69
+ return Value.foreign(() => s.toLowerCase())
70
+ }
71
+ if (index === 'toUpperCase') {
72
+ return Value.foreign(() => s.toUpperCase())
73
+ }
74
+ if (index === 'trim') {
75
+ return Value.foreign(() => s.trim())
76
+ }
77
+ if (index === 'trimEnd') {
78
+ return Value.foreign(() => s.trimEnd())
79
+ }
80
+ if (index === 'trimStart') {
81
+ return Value.foreign(() => s.trimStart())
82
+ }
83
+ throw new Error(`Unrecognized string method: ${index}`)
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './septima'
@@ -0,0 +1,13 @@
1
+ export interface Location {
2
+ readonly offset: number
3
+ }
4
+
5
+ export interface Span {
6
+ from: Location
7
+ to: Location
8
+ }
9
+
10
+ export interface Location2d {
11
+ line: number
12
+ col: number
13
+ }