[WIP] Start implementing support for lambda parsing
This commit is contained in:
@@ -26,5 +26,9 @@
|
|||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"rimraf": "^6.1.0",
|
"rimraf": "^6.1.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansis": "^4.2.0",
|
||||||
|
"table": "^6.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
77
pnpm-lock.yaml
generated
77
pnpm-lock.yaml
generated
@@ -7,6 +7,13 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
ansis:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.0
|
||||||
|
table:
|
||||||
|
specifier: ^6.9.0
|
||||||
|
version: 6.9.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22
|
specifier: ^22
|
||||||
@@ -35,6 +42,9 @@ packages:
|
|||||||
'@types/node@22.19.0':
|
'@types/node@22.19.0':
|
||||||
resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==}
|
resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==}
|
||||||
|
|
||||||
|
ajv@8.18.0:
|
||||||
|
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||||
|
|
||||||
ansi-regex@5.0.1:
|
ansi-regex@5.0.1:
|
||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -51,6 +61,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ansis@4.2.0:
|
||||||
|
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
astral-regex@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -71,6 +89,12 @@ packages:
|
|||||||
emoji-regex@9.2.2:
|
emoji-regex@9.2.2:
|
||||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3:
|
||||||
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
fast-uri@3.1.0:
|
||||||
|
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -91,6 +115,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
json-schema-traverse@1.0.0:
|
||||||
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
|
lodash.truncate@4.4.2:
|
||||||
|
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
|
||||||
|
|
||||||
lru-cache@11.2.2:
|
lru-cache@11.2.2:
|
||||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -114,6 +144,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
require-from-string@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
rimraf@6.1.0:
|
rimraf@6.1.0:
|
||||||
resolution: {integrity: sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==}
|
resolution: {integrity: sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -131,6 +165,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
slice-ansi@4.0.0:
|
||||||
|
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -147,6 +185,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
table@6.9.0:
|
||||||
|
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -189,6 +231,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
ajv@8.18.0:
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
fast-uri: 3.1.0
|
||||||
|
json-schema-traverse: 1.0.0
|
||||||
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
ansi-regex@6.2.2: {}
|
ansi-regex@6.2.2: {}
|
||||||
@@ -199,6 +248,10 @@ snapshots:
|
|||||||
|
|
||||||
ansi-styles@6.2.3: {}
|
ansi-styles@6.2.3: {}
|
||||||
|
|
||||||
|
ansis@4.2.0: {}
|
||||||
|
|
||||||
|
astral-regex@2.0.0: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -217,6 +270,10 @@ snapshots:
|
|||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
|
fast-uri@3.1.0: {}
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@@ -239,6 +296,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
|
|
||||||
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
|
lodash.truncate@4.4.2: {}
|
||||||
|
|
||||||
lru-cache@11.2.2: {}
|
lru-cache@11.2.2: {}
|
||||||
|
|
||||||
minimatch@10.1.1:
|
minimatch@10.1.1:
|
||||||
@@ -256,6 +317,8 @@ snapshots:
|
|||||||
lru-cache: 11.2.2
|
lru-cache: 11.2.2
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
|
|
||||||
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
rimraf@6.1.0:
|
rimraf@6.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 11.0.3
|
glob: 11.0.3
|
||||||
@@ -269,6 +332,12 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
slice-ansi@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
astral-regex: 2.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
@@ -289,6 +358,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 6.2.2
|
ansi-regex: 6.2.2
|
||||||
|
|
||||||
|
table@6.9.0:
|
||||||
|
dependencies:
|
||||||
|
ajv: 8.18.0
|
||||||
|
lodash.truncate: 4.4.2
|
||||||
|
slice-ansi: 4.0.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
|
import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
|
||||||
|
|
||||||
export const log: Logger = new ConsoleLogger(LogLevel.VERBOSE)
|
export const log: Logger = new ConsoleLogger(LogLevel.ERROR)
|
||||||
log.setStreamLevel('lexer', LogLevel.INFO)
|
// log.setStreamLevel('lexer', LogLevel.INFO)
|
||||||
log.setStreamLevel('token', LogLevel.INFO)
|
// log.setStreamLevel('token', LogLevel.INFO)
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import {createHash} from 'node:crypto';
|
import {createHash} from 'node:crypto';
|
||||||
import {LexInput, tokenIsLVal} from '../lexer.js'
|
import {LexInput, LexToken, tokenIsLVal} from '../lexer.js'
|
||||||
import {
|
import {
|
||||||
Executable,
|
Executable,
|
||||||
ExpectedEndOfInputError,
|
ExpectedEndOfInputError,
|
||||||
InvalidVariableNameError,
|
InvalidVariableNameError,
|
||||||
IsNotKeywordError,
|
IsNotKeywordError,
|
||||||
UnexpectedEndOfInputError
|
UnexpectedEndOfInputError, UnexpectedEndofStatementError
|
||||||
} from "../parse.js";
|
} from "../parse.js";
|
||||||
import {Awaitable, ElementType, hasOwnProperty} from "../../util/types.js";
|
import {Awaitable, ElementType, hasOwnProperty} from "../../util/types.js";
|
||||||
import {StrVM} from "../vm.js";
|
import {StrVM} from "../vm.js";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
|
export class TypeError extends Error {}
|
||||||
|
|
||||||
export type StrLVal = { term: 'variable', name: string }
|
export type StrLVal = { term: 'variable', name: string }
|
||||||
|
|
||||||
export const isStrLVal = (val: unknown): val is StrLVal =>
|
export const isStrLVal = (val: unknown): val is StrLVal =>
|
||||||
@@ -19,26 +21,64 @@ export const isStrLVal = (val: unknown): val is StrLVal =>
|
|||||||
&& hasOwnProperty(val, 'term') && val.term === 'variable'
|
&& hasOwnProperty(val, 'term') && val.term === 'variable'
|
||||||
&& hasOwnProperty(val, 'name') && typeof val.name === 'string')
|
&& hasOwnProperty(val, 'name') && typeof val.name === 'string')
|
||||||
|
|
||||||
export type StrDestructured = { term: 'destructured', value: { prefix?: string, value: string }[] }
|
export type StrDestructured = { term: 'destructured', value: { prefix?: string, value: StrRVal }[] }
|
||||||
|
|
||||||
export const joinDestructured = (val: StrDestructured['value']): string =>
|
export const joinDestructured = (val: StrDestructured['value']): string =>
|
||||||
val
|
val
|
||||||
.map(part => `${part.prefix || ''}${part.value}`)
|
.map(part => `${part.prefix || ''}${part.value.value}`)
|
||||||
.join('')
|
.join('')
|
||||||
|
|
||||||
export const destructureToLines = (val: string): StrDestructured['value'] => val
|
export const destructureToLines = (val: string): StrDestructured['value'] => val
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line, idx) => {
|
.map((line, idx) => {
|
||||||
if ( idx ) {
|
if ( idx ) {
|
||||||
return { prefix: '\n', value: line }
|
return { prefix: '\n', value: wrapString(line) }
|
||||||
}
|
}
|
||||||
return { value: line }
|
return { value: wrapString(line) }
|
||||||
})
|
})
|
||||||
|
|
||||||
export type StrRVal =
|
export type StrString = { term: 'string', value: string, literal?: true }
|
||||||
{ term: 'string', value: string, literal?: true }
|
|
||||||
| { term: 'int', value: number }
|
export type StrInt = { term: 'int', value: number }
|
||||||
| StrDestructured
|
|
||||||
|
export type StrLamba = {
|
||||||
|
term: 'lambda',
|
||||||
|
value: {
|
||||||
|
args: StrLVal[],
|
||||||
|
exec: Executable<CommandData>[]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StrRVal = StrString | StrInt | StrDestructured | StrLamba
|
||||||
|
|
||||||
|
export type StrDestructuredTable = {
|
||||||
|
term: 'destructured',
|
||||||
|
value: {
|
||||||
|
prefix?: string,
|
||||||
|
value: {
|
||||||
|
term: 'destructured',
|
||||||
|
value: {
|
||||||
|
prefix?: string,
|
||||||
|
value: StrString,
|
||||||
|
}[],
|
||||||
|
},
|
||||||
|
}[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isStrDestructuredTable = (what: StrRVal): what is StrDestructuredTable => {
|
||||||
|
return what.term === 'destructured'
|
||||||
|
&& what.value.every(item =>
|
||||||
|
item.value.term === 'destructured'
|
||||||
|
&& item.value.value.every(subitem =>
|
||||||
|
subitem.value.term === 'string' || subitem.value.term === 'int'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unwrapStrDestructuredTable = (table: StrDestructuredTable): string[][] => {
|
||||||
|
return table.value
|
||||||
|
.map(row =>
|
||||||
|
row.value.value
|
||||||
|
.map(cell => unwrapString(cell.value)))
|
||||||
|
}
|
||||||
|
|
||||||
const toHex = (v: string) => createHash('sha256').update(v).digest('hex')
|
const toHex = (v: string) => createHash('sha256').update(v).digest('hex')
|
||||||
|
|
||||||
@@ -51,6 +91,10 @@ export const hashStrRVal = (val: StrRVal): string => {
|
|||||||
return toHex(`s:int:${val.value}`)
|
return toHex(`s:int:${val.value}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( val.term === 'lambda' ) {
|
||||||
|
throw new Error('Cannot hash lambda') // todo
|
||||||
|
}
|
||||||
|
|
||||||
return toHex(`s:dstr:${joinDestructured(val.value)}`)
|
return toHex(`s:dstr:${joinDestructured(val.value)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +117,8 @@ export const unwrapString = (term: StrRVal): string => {
|
|||||||
return String(term.value)
|
return String(term.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( term.term === 'destructured' ) {
|
if ( term.term === 'destructured' || term.term === 'lambda' ) {
|
||||||
throw new Error('ope!') // fixme
|
throw new TypeError(`Found unexpected ${term.term} (expected: string|int)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return term.value
|
return term.value
|
||||||
@@ -95,7 +139,7 @@ export const wrapInt = (val: number): StrRVal => ({
|
|||||||
|
|
||||||
export const unwrapInt = (term: StrRVal): number => {
|
export const unwrapInt = (term: StrRVal): number => {
|
||||||
if ( term.term !== 'int' ) {
|
if ( term.term !== 'int' ) {
|
||||||
throw new Error('Unexpected error: cannot unwrap term: is not an int')
|
throw new TypeError(`Found unexpected ${term.term} (expected: int)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return term.value
|
return term.value
|
||||||
@@ -108,7 +152,7 @@ export const wrapDestructured = (val: StrDestructured['value']): StrDestructured
|
|||||||
|
|
||||||
export const unwrapDestructured = (term: StrRVal): StrDestructured['value'] => {
|
export const unwrapDestructured = (term: StrRVal): StrDestructured['value'] => {
|
||||||
if ( term.term !== 'destructured' ) {
|
if ( term.term !== 'destructured' ) {
|
||||||
throw new Error('Unexpected error: cannot unwrap term: is not a destructured')
|
throw new TypeError(`Found unexpected ${term.term} (expected: destructured)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return term.value
|
return term.value
|
||||||
@@ -127,15 +171,20 @@ export const processPath = (path: string): string => {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParseSubContext {
|
||||||
|
inputs: LexToken[],
|
||||||
|
}
|
||||||
|
|
||||||
export class ParseContext {
|
export class ParseContext {
|
||||||
constructor(
|
constructor(
|
||||||
private inputs: LexInput[],
|
private inputs: LexToken[],
|
||||||
private childParser: (tokens: LexInput[]) => Awaitable<[Executable<CommandData>, LexInput[]]>,
|
private childParser: (tokens: LexToken[]) => Awaitable<[Executable<CommandData>, LexToken[]]>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
assertEmpty() {
|
assertEmpty() {
|
||||||
if ( this.inputs.length ) {
|
if ( this.inputs.length ) {
|
||||||
throw new ExpectedEndOfInputError(`Expected end of input. Found: ${this.inputs[0].value}`)
|
const showTerm = this.inputs[0].type === 'terminator' ? 'EOS' : this.inputs[0].value
|
||||||
|
throw new ExpectedEndOfInputError(`Expected end of input. Found: ${showTerm}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +205,11 @@ export class ParseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const input = this.inputs.shift()!
|
const input = this.inputs.shift()!
|
||||||
|
|
||||||
|
if ( input.type === 'terminator' ) {
|
||||||
|
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected term.')
|
||||||
|
}
|
||||||
|
|
||||||
return this.parseInputToTerm(input)
|
return this.parseInputToTerm(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +219,10 @@ export class ParseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const input = this.inputs[0]
|
const input = this.inputs[0]
|
||||||
|
if ( input.type === 'terminator' ) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
return this.parseInputToTerm(input)
|
return this.parseInputToTerm(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +256,9 @@ export class ParseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const input = this.inputs.shift()!
|
const input = this.inputs.shift()!
|
||||||
|
if ( input.type === 'terminator' ) {
|
||||||
|
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected one of: ' + options.join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
if ( input.literal || !options.includes(input.value) ) {
|
if ( input.literal || !options.includes(input.value) ) {
|
||||||
throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')')
|
throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')')
|
||||||
@@ -208,10 +269,14 @@ export class ParseContext {
|
|||||||
|
|
||||||
popLVal(): StrLVal {
|
popLVal(): StrLVal {
|
||||||
if ( !this.inputs.length ) {
|
if ( !this.inputs.length ) {
|
||||||
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected lval.');
|
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected lval.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = this.inputs.shift()!
|
const input = this.inputs.shift()!
|
||||||
|
if ( input.type === 'terminator' ) {
|
||||||
|
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected lval.')
|
||||||
|
}
|
||||||
|
|
||||||
if ( !tokenIsLVal(input) ) {
|
if ( !tokenIsLVal(input) ) {
|
||||||
throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`)
|
throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`)
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/vm/commands/concat.ts
Normal file
43
src/vm/commands/concat.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {Command, ParseContext, StrTerm, wrapString} from "./command.js";
|
||||||
|
import {LexInput} from "../lexer.js";
|
||||||
|
import {StrVM} from "../vm.js";
|
||||||
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
|
export type ConcatData = {
|
||||||
|
terms: StrTerm[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Concat extends Command<ConcatData> {
|
||||||
|
isParseCandidate(token: LexInput): boolean {
|
||||||
|
return this.isKeyword(token, 'concat') || this.isKeyword(token, 'cat')
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptParse(context: ParseContext): ConcatData {
|
||||||
|
const data: ConcatData = {
|
||||||
|
terms: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
let term: StrTerm|undefined
|
||||||
|
while ( term = context.popOptionalTerm() ) {
|
||||||
|
data.terms.push(term)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayName(): string {
|
||||||
|
return 'concat'
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(vm: StrVM, data: ConcatData): Awaitable<StrVM> {
|
||||||
|
return vm.replaceContextMatchingTerm(ctx => ({
|
||||||
|
override: () => {
|
||||||
|
const result = data.terms
|
||||||
|
.map(term => ctx.resolveString(term))
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
return wrapString(result)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ export class Contains extends Command<{ find: StrTerm }> {
|
|||||||
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
|
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
|
||||||
return vm.replaceContextMatchingTerm(ctx => ({
|
return vm.replaceContextMatchingTerm(ctx => ({
|
||||||
string: sub => sub.includes(ctx.resolveString(data.find)) ? sub : '',
|
string: sub => sub.includes(ctx.resolveString(data.find)) ? sub : '',
|
||||||
destructured: parts => parts.filter(part =>
|
destructuredOfStrings: parts => parts.filter(part =>
|
||||||
part.value.includes(ctx.resolveString(data.find))),
|
part.includes(ctx.resolveString(data.find))),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class Each extends Command<EachData> {
|
|||||||
await child.replaceContextMatchingTerm({ override: part.value })
|
await child.replaceContextMatchingTerm({ override: part.value })
|
||||||
return child.runInPlace(async ctx => {
|
return child.runInPlace(async ctx => {
|
||||||
await data.exec.command.execute(child, data.exec.data)
|
await data.exec.command.execute(child, data.exec.data)
|
||||||
return unwrapString(ctx.getSubject())
|
return ctx.getSubject()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {Prefix} from "./prefix.js";
|
|||||||
import {Quote} from "./quote.js";
|
import {Quote} from "./quote.js";
|
||||||
import {Redo} from "./redo.js";
|
import {Redo} from "./redo.js";
|
||||||
import {Replace} from "./replace.js";
|
import {Replace} from "./replace.js";
|
||||||
|
import {Reverse} from "./rev.js";
|
||||||
import {RSub} from "./rsub.js";
|
import {RSub} from "./rsub.js";
|
||||||
import {Show} from "./show.js";
|
import {Show} from "./show.js";
|
||||||
import {Split} from "./split.js";
|
import {Split} from "./split.js";
|
||||||
@@ -46,11 +47,13 @@ import {Sort} from "./sort.js";
|
|||||||
import {Set} from "./set.js";
|
import {Set} from "./set.js";
|
||||||
import {Assign} from "./assign.js";
|
import {Assign} from "./assign.js";
|
||||||
import {Zip} from "./zip.js";
|
import {Zip} from "./zip.js";
|
||||||
|
import {Concat} from "./concat.js";
|
||||||
|
|
||||||
export type Commands = Command<CommandData>[]
|
export type Commands = Command<CommandData>[]
|
||||||
export const commands: Commands = [
|
export const commands: Commands = [
|
||||||
new Assign,
|
new Assign,
|
||||||
new Clear,
|
new Clear,
|
||||||
|
new Concat,
|
||||||
new Contains,
|
new Contains,
|
||||||
new Copy,
|
new Copy,
|
||||||
new Drop,
|
new Drop,
|
||||||
@@ -79,6 +82,7 @@ export const commands: Commands = [
|
|||||||
new Quote,
|
new Quote,
|
||||||
new Redo,
|
new Redo,
|
||||||
new Replace,
|
new Replace,
|
||||||
|
new Reverse,
|
||||||
new RSub,
|
new RSub,
|
||||||
new RunFile,
|
new RunFile,
|
||||||
new Save,
|
new Save,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class Join extends Command<{ with?: StrTerm }> {
|
|||||||
restructureOrLines: parts => {
|
restructureOrLines: parts => {
|
||||||
if ( data.with ) {
|
if ( data.with ) {
|
||||||
return parts
|
return parts
|
||||||
.map(part => part.value)
|
.map(part => part.value.value)
|
||||||
.join(ctx.resolveString(data.with))
|
.join(ctx.resolveString(data.with))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Command, ParseContext, unwrapString} from "./command.js";
|
import {Command, ParseContext, wrapString} from "./command.js";
|
||||||
import {LexInput} from "../lexer.js";
|
import {LexInput} from "../lexer.js";
|
||||||
import {StrVM} from "../vm.js";
|
import {StrVM} from "../vm.js";
|
||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
@@ -22,7 +22,7 @@ export class Lines extends Command<{}> {
|
|||||||
return sub.split('\n')
|
return sub.split('\n')
|
||||||
.map((line, idx) => ({
|
.map((line, idx) => ({
|
||||||
prefix: idx ? '\n' : undefined,
|
prefix: idx ? '\n' : undefined,
|
||||||
value: line,
|
value: wrapString(line),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export class Missing extends Command<{ find: StrTerm }> {
|
|||||||
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
|
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
|
||||||
return vm.replaceContextMatchingTerm(ctx => ({
|
return vm.replaceContextMatchingTerm(ctx => ({
|
||||||
string: sub => sub.includes(ctx.resolveString(data.find)) ? '' : sub,
|
string: sub => sub.includes(ctx.resolveString(data.find)) ? '' : sub,
|
||||||
destructured: parts => parts.filter(part =>
|
destructuredOfStrings: parts => parts.filter(part =>
|
||||||
!part.value.includes(ctx.resolveString(data.find))),
|
!part.includes(ctx.resolveString(data.find))),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ export class On extends Command<OnData> {
|
|||||||
|
|
||||||
// Apply the command to the value of the given index:
|
// Apply the command to the value of the given index:
|
||||||
const result = await vm.runInChild(async (child, childCtx) => {
|
const result = await vm.runInChild(async (child, childCtx) => {
|
||||||
await childCtx.replaceSubject(() => wrapString(operand.value))
|
await childCtx.replaceSubject(() => operand.value)
|
||||||
await data.exec.command.execute(child, data.exec.data)
|
await data.exec.command.execute(child, data.exec.data)
|
||||||
return unwrapString(childCtx.getSubject())
|
return childCtx.getSubject()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Replace the specific index back into the destructured:
|
// Replace the specific index back into the destructured:
|
||||||
|
|||||||
25
src/vm/commands/rev.ts
Normal file
25
src/vm/commands/rev.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {Command} from "./command.js";
|
||||||
|
import {LexInput} from "../lexer.js";
|
||||||
|
import {StrVM} from "../vm.js";
|
||||||
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
|
export class Reverse extends Command<{}> {
|
||||||
|
attemptParse(): {} {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayName(): string {
|
||||||
|
return 'rev'
|
||||||
|
}
|
||||||
|
|
||||||
|
isParseCandidate(token: LexInput): boolean {
|
||||||
|
return this.isKeyword(token, 'rev')
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(vm: StrVM): Awaitable<StrVM> {
|
||||||
|
return vm.replaceContextMatchingTerm({
|
||||||
|
string: s => s.split('').reverse().join(''),
|
||||||
|
destructured: s => [...s].reverse(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Command, ParseContext, StrTerm, unwrapString, wrapDestructured} from "./command.js";
|
import {Command, ParseContext, StrTerm, wrapString} from "./command.js";
|
||||||
import {LexInput} from "../lexer.js";
|
import {LexInput} from "../lexer.js";
|
||||||
import {StrVM} from "../vm.js";
|
import {StrVM} from "../vm.js";
|
||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
@@ -30,7 +30,7 @@ export class Split extends Command<SplitData> {
|
|||||||
return sub.split(prefix)
|
return sub.split(prefix)
|
||||||
.map((segment, idx) => ({
|
.map((segment, idx) => ({
|
||||||
prefix: idx ? prefix : undefined,
|
prefix: idx ? prefix : undefined,
|
||||||
value: segment,
|
value: wrapString(segment),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class Unique extends Command<{}> {
|
|||||||
destructuredOrLines: sub => {
|
destructuredOrLines: sub => {
|
||||||
const seen: Record<string, boolean> = {}
|
const seen: Record<string, boolean> = {}
|
||||||
return sub.filter(part => {
|
return sub.filter(part => {
|
||||||
const hash = hashStrRVal(wrapString(part.value))
|
const hash = hashStrRVal(part.value)
|
||||||
if ( seen[hash] ) {
|
if ( seen[hash] ) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Command, ParseContext} from "./command.js";
|
import {Command, ParseContext, wrapString} from "./command.js";
|
||||||
import {LexInput} from "../lexer.js";
|
import {LexInput} from "../lexer.js";
|
||||||
import {StrVM} from "../vm.js";
|
import {StrVM} from "../vm.js";
|
||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
@@ -24,7 +24,7 @@ export class Words extends Command<{}> {
|
|||||||
|
|
||||||
return parts.map((part, idx) => ({
|
return parts.map((part, idx) => ({
|
||||||
prefix: idx ? separators[idx - 1][0] : undefined,
|
prefix: idx ? separators[idx - 1][0] : undefined,
|
||||||
value: part,
|
value: wrapString(part),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export class Input extends BehaviorSubject<string> implements LifecycleAware {
|
|||||||
private rl?: readline.Interface
|
private rl?: readline.Interface
|
||||||
private log: StreamLogger = log.getStreamLogger('input')
|
private log: StreamLogger = log.getStreamLogger('input')
|
||||||
|
|
||||||
|
public readonly errors$: BehaviorSubject<Error> = new BehaviorSubject()
|
||||||
|
|
||||||
public hasPrompt(): boolean {
|
public hasPrompt(): boolean {
|
||||||
return !!this.rl
|
return !!this.rl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,86 @@
|
|||||||
import {StrRVal} from "./commands/command.js";
|
import {isStrDestructuredTable, StrRVal, unwrapStrDestructuredTable} from "./commands/command.js";
|
||||||
import {Awaitable} from "../util/types.js";
|
import {Awaitable} from "../util/types.js";
|
||||||
import childProcess from "node:child_process";
|
import childProcess from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import {tempFile} from "../util/fs.js";
|
import {tempFile} from "../util/fs.js";
|
||||||
|
import {table} from "table";
|
||||||
|
import * as ansi from 'ansis';
|
||||||
|
|
||||||
export const getSubjectDisplay = (sub: StrRVal): string => {
|
export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => {
|
||||||
let annotated = '\n┌───────────────\n'
|
if ( typeof firstLinePrefix === 'undefined' ) {
|
||||||
|
firstLinePrefix = prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isStrDestructuredTable(sub) ) {
|
||||||
|
const config = {
|
||||||
|
border: {
|
||||||
|
topBody: ansi.gray`─`,
|
||||||
|
topJoin: ansi.gray`┬`,
|
||||||
|
topLeft: ansi.gray`${firstLinePrefix}┌`,
|
||||||
|
topRight: ansi.gray`┐`,
|
||||||
|
|
||||||
|
bottomBody: ansi.gray`─`,
|
||||||
|
bottomJoin: ansi.gray`┴`,
|
||||||
|
bottomLeft: ansi.gray`${prefix}└`,
|
||||||
|
bottomRight: ansi.gray`┘`,
|
||||||
|
|
||||||
|
bodyLeft: ansi.gray`${prefix}│`,
|
||||||
|
bodyRight: ansi.gray`│`,
|
||||||
|
bodyJoin: ansi.gray`│`,
|
||||||
|
|
||||||
|
joinBody: ansi.gray`─`,
|
||||||
|
joinLeft: ansi.gray`${prefix}├`,
|
||||||
|
joinRight: ansi.gray`┤`,
|
||||||
|
joinJoin: ansi.gray`┼`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let annotatedTable = unwrapStrDestructuredTable(sub)
|
||||||
|
.map((row, rowIdx) => [
|
||||||
|
ansi.blue`${rowIdx.toString()}`,
|
||||||
|
...row,
|
||||||
|
])
|
||||||
|
|
||||||
|
annotatedTable = [
|
||||||
|
['', ...annotatedTable[0]
|
||||||
|
.map((cell, cellIdx) => ansi.blue`${(cellIdx - 1).toString()}`)
|
||||||
|
.slice(1)],
|
||||||
|
...annotatedTable,
|
||||||
|
]
|
||||||
|
|
||||||
|
return table(annotatedTable, config)
|
||||||
|
+ `${prefix}├────────────────────────────────────────────────`
|
||||||
|
+`\n${prefix}│ :: destructured (:: destructured (:: string))`
|
||||||
|
+ `\n${prefix}└────────────────────────────────────────────────`
|
||||||
|
}
|
||||||
|
|
||||||
|
let annotated = firstLinePrefix + '┌───────────────'
|
||||||
if ( sub.term === 'string' ) {
|
if ( sub.term === 'string' ) {
|
||||||
const lines = sub.value.split('\n')
|
const lines = sub.value.split('\n')
|
||||||
const padLength = `${lines.length}`.length // heh
|
const padLength = `${lines.length}`.length // heh
|
||||||
annotated += lines
|
annotated += '\n' + lines
|
||||||
.map((line, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line)
|
.map((line, idx) => prefix + '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( sub.term === 'int' ) {
|
if ( sub.term === 'int' ) {
|
||||||
annotated += `│ ${sub.value}`
|
annotated += prefix + `\n│ ${sub.value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( sub.term === 'destructured' ) {
|
if ( sub.term === 'destructured' ) {
|
||||||
const padLength = `${sub.value.length}`.length
|
const padLength = `${sub.value.length}`.length
|
||||||
annotated += sub.value
|
annotated += '\n' + sub.value
|
||||||
.map((el, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │'
|
.map((el, elIdx) => {
|
||||||
+ el.value.split('\n').map((line, lineIdx) => lineIdx ? (`│ ${''.padStart(padLength, ' ')} │${line}`) : line).join('\n'))
|
const subPrefix = prefix + `│ ${''.padStart(padLength, ' ')} │`
|
||||||
.join('\n│ ' + ''.padStart(padLength, ' ') + ' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n')
|
const subFirstPrefix = prefix + `│ ${elIdx.toString().padStart(padLength, ' ')} │`
|
||||||
|
return getSubjectDisplay(el.value, subPrefix, subFirstPrefix)
|
||||||
|
})
|
||||||
|
.join(`\n${prefix}│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
annotated += '\n├───────────────'
|
annotated += `\n${prefix}├───────────────`
|
||||||
annotated += `\n│ :: ${sub.term}`
|
annotated += `\n${prefix}│ :: ${sub.term}`
|
||||||
annotated += '\n└───────────────'
|
annotated += `\n${prefix}└───────────────`
|
||||||
return annotated
|
return annotated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export class InternalParseError extends ParseError {}
|
|||||||
export class IsNotKeywordError extends ParseError {}
|
export class IsNotKeywordError extends ParseError {}
|
||||||
export class InvalidCommandError extends ParseError {}
|
export class InvalidCommandError extends ParseError {}
|
||||||
export class UnexpectedEndOfInputError extends ParseError {}
|
export class UnexpectedEndOfInputError extends ParseError {}
|
||||||
|
export class UnexpectedEndofStatementError extends ParseError {}
|
||||||
export class ExpectedEndOfInputError extends InvalidCommandError {}
|
export class ExpectedEndOfInputError extends InvalidCommandError {}
|
||||||
export class InvalidVariableNameError extends ParseError {}
|
export class InvalidVariableNameError extends ParseError {}
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Executable,
|
Executable,
|
||||||
InternalParseError,
|
InternalParseError,
|
||||||
InvalidCommandError,
|
InvalidCommandError,
|
||||||
IsNotKeywordError,
|
IsNotKeywordError, ParseError,
|
||||||
UnexpectedEndOfInputError
|
UnexpectedEndOfInputError
|
||||||
} from './parse.js'
|
} from './parse.js'
|
||||||
|
|
||||||
@@ -16,15 +16,41 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
|||||||
private logger: StreamLogger
|
private logger: StreamLogger
|
||||||
|
|
||||||
private parseCandidate?: Command<CommandData>
|
private parseCandidate?: Command<CommandData>
|
||||||
private inputForCandidate: LexInput[] = []
|
private inputForCandidate: LexToken[] = []
|
||||||
|
|
||||||
|
private subcontextLevel: number = 0;
|
||||||
|
|
||||||
|
/** Used when no parse candidate is found. Prevents trying to parse the tail of the command. */
|
||||||
|
private dropUntilTerminator: boolean = false
|
||||||
|
|
||||||
constructor(private commands: Commands, lexer?: Lexer) {
|
constructor(private commands: Commands, lexer?: Lexer) {
|
||||||
super()
|
super()
|
||||||
this.logger = log.getStreamLogger('parser')
|
this.logger = log.getStreamLogger('parser')
|
||||||
lexer?.subscribe(token => this.handleToken(token))
|
lexer?.subscribe({
|
||||||
|
next: token => this.handleToken(token),
|
||||||
|
error: error => this.handleParseError(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleParseError(error: Error) {
|
||||||
|
if ( error instanceof ParseError ) {
|
||||||
|
this.logger.error(`(${error.constructor.name}) ${error.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleToken(token: LexToken) {
|
async handleToken(token: LexToken) {
|
||||||
|
// We previously encountered an invalid command, so avoid trying to parse the tail of it:
|
||||||
|
if ( this.dropUntilTerminator ) {
|
||||||
|
if ( token.type === 'terminator' ) {
|
||||||
|
this.dropUntilTerminator = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// We are in between full commands, so try to identify a new parse candidate:
|
// We are in between full commands, so try to identify a new parse candidate:
|
||||||
if ( !this.parseCandidate ) {
|
if ( !this.parseCandidate ) {
|
||||||
// Ignore duplicated terminators between commands
|
// Ignore duplicated terminators between commands
|
||||||
@@ -37,7 +63,13 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
|||||||
throw new IsNotKeywordError('Expected keyword, found: ' + this.displayToken(token))
|
throw new IsNotKeywordError('Expected keyword, found: ' + this.displayToken(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
this.parseCandidate = this.getParseCandidate(token)
|
this.parseCandidate = this.getParseCandidate(token)
|
||||||
|
} catch (e) {
|
||||||
|
this.dropUntilTerminator = true
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
|
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
|
||||||
this.inputForCandidate.push(token)
|
this.inputForCandidate.push(token)
|
||||||
}
|
}
|
||||||
@@ -48,13 +80,28 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
|||||||
// If this is normal input token, collect it so we can give it to the candidate to parse:
|
// If this is normal input token, collect it so we can give it to the candidate to parse:
|
||||||
if ( token.type === 'input' ) {
|
if ( token.type === 'input' ) {
|
||||||
this.inputForCandidate.push(token)
|
this.inputForCandidate.push(token)
|
||||||
|
|
||||||
|
if ( !token.literal && token.value.startsWith('(') ) {
|
||||||
|
this.subcontextLevel += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !token.literal && token.value.endsWith(')') && this.subcontextLevel ) {
|
||||||
|
this.subcontextLevel -= 1
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got a terminator, then ask the candidate to actually perform its parse:
|
// If we got a terminator, then ask the candidate to actually perform its parse:
|
||||||
if ( token.type === 'terminator' ) {
|
if ( token.type === 'terminator' ) {
|
||||||
|
if ( this.subcontextLevel > 0 ) {
|
||||||
|
// We're inside a sub-context right now, so just accumulate the input and continue on:
|
||||||
|
this.inputForCandidate.push(token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Have the candidate attempt to parse itself from the collecte data:
|
// Have the candidate attempt to parse itself from the collected data:
|
||||||
const context = this.getContext()
|
const context = this.getContext()
|
||||||
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
|
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
|
||||||
const data = await this.parseCandidate.attemptParse(context)
|
const data = await this.parseCandidate.attemptParse(context)
|
||||||
|
|||||||
90
src/vm/vm.ts
90
src/vm/vm.ts
@@ -1,10 +1,10 @@
|
|||||||
import {Awaitable, JSONData} from "../util/types.js";
|
import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js";
|
||||||
import {
|
import {
|
||||||
CommandData, destructureToLines,
|
CommandData, destructureToLines, isStrLVal,
|
||||||
isStrRVal, joinDestructured, StrDestructured,
|
isStrRVal, joinDestructured, StrDestructured,
|
||||||
StrLVal,
|
StrLVal,
|
||||||
StrRVal,
|
StrRVal,
|
||||||
StrTerm, unwrapDestructured,
|
StrTerm, TypeError, unwrapDestructured,
|
||||||
unwrapInt,
|
unwrapInt,
|
||||||
unwrapString, wrapDestructured, wrapInt,
|
unwrapString, wrapDestructured, wrapInt,
|
||||||
wrapString
|
wrapString
|
||||||
@@ -82,6 +82,8 @@ export type TermOperator = {
|
|||||||
* If `destructured`, map directly.
|
* If `destructured`, map directly.
|
||||||
*/
|
*/
|
||||||
restructureOrLines?: (sub: StrDestructured['value']) => Awaitable<string>,
|
restructureOrLines?: (sub: StrDestructured['value']) => Awaitable<string>,
|
||||||
|
/** Map `destructured` of `string` to `destructured` of `string`. */
|
||||||
|
destructuredOfStrings?: (sub: string[]) => Awaitable<string[]>,
|
||||||
/** Map `destructured` to `destructured`. */
|
/** Map `destructured` to `destructured`. */
|
||||||
destructured?: (sub: StrDestructured['value']) => Awaitable<StrDestructured['value']>,
|
destructured?: (sub: StrDestructured['value']) => Awaitable<StrDestructured['value']>,
|
||||||
/**
|
/**
|
||||||
@@ -98,6 +100,38 @@ export type TermOperator = {
|
|||||||
override?: string | StrRVal | ((sub: StrRVal) => Awaitable<StrRVal>),
|
override?: string | StrRVal | ((sub: StrRVal) => Awaitable<StrRVal>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const termOperatorInputDisplays: Record<keyof TermOperator, string[]> = {
|
||||||
|
destructure: ['string'],
|
||||||
|
int: ['int'],
|
||||||
|
string: ['string', 'int'],
|
||||||
|
restructure: ['destructured'],
|
||||||
|
restructureOrLines: ['string', 'destructured'],
|
||||||
|
destructured: ['destructured'],
|
||||||
|
destructuredOfStrings: ['destructured'],
|
||||||
|
destructuredOrLines: ['string', 'destructured'],
|
||||||
|
stringOrDestructuredPart: ['string', 'destructured'],
|
||||||
|
override: ['string', 'int', 'destructured'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTermOperatorInputDisplayList = (op: TermOperator): string[] => {
|
||||||
|
const vals: Partial<Record<string, true>> = {}
|
||||||
|
|
||||||
|
let key: keyof TermOperator
|
||||||
|
// @ts-ignore
|
||||||
|
for ( key of Object.keys(op) ) {
|
||||||
|
for ( const disp of termOperatorInputDisplays[key] ) {
|
||||||
|
vals[disp] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ vals })
|
||||||
|
return Object.keys(vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExecutionError extends Error {}
|
||||||
|
export class TermOperationError extends ExecutionError {}
|
||||||
|
export class UndefinedTermError extends ExecutionError {}
|
||||||
|
|
||||||
export class ExecutionContext {
|
export class ExecutionContext {
|
||||||
private history: [StrRVal, Scope][] = []
|
private history: [StrRVal, Scope][] = []
|
||||||
private forwardHistory: [StrRVal, Scope][] = []
|
private forwardHistory: [StrRVal, Scope][] = []
|
||||||
@@ -214,6 +248,29 @@ export class ExecutionContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
sub.term === 'destructured'
|
||||||
|
&& (sub.value.length < 1 || sub.value[0].value.term === 'string')
|
||||||
|
&& operator.destructuredOfStrings
|
||||||
|
) {
|
||||||
|
const prefixes = unwrapDestructured(sub)
|
||||||
|
.map(v => v.prefix)
|
||||||
|
|
||||||
|
const strings = unwrapDestructured(sub)
|
||||||
|
.map(v => v.value.value as string)
|
||||||
|
|
||||||
|
const mappedStrings = (await operator.destructuredOfStrings(strings))
|
||||||
|
.map(string => wrapString(string))
|
||||||
|
|
||||||
|
this.subject = wrapDestructured(
|
||||||
|
mappedStrings.map((value, idx) => ({
|
||||||
|
prefix: prefixes[idx],
|
||||||
|
value,
|
||||||
|
})))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ( sub.term === 'destructured' && operator.destructured ) {
|
if ( sub.term === 'destructured' && operator.destructured ) {
|
||||||
this.subject = wrapDestructured(await operator.destructured(unwrapDestructured(sub)))
|
this.subject = wrapDestructured(await operator.destructured(unwrapDestructured(sub)))
|
||||||
return
|
return
|
||||||
@@ -224,12 +281,16 @@ export class ExecutionContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( sub.term === 'destructured' && operator.stringOrDestructuredPart ) {
|
if (
|
||||||
|
sub.term === 'destructured'
|
||||||
|
&& (sub.value.length < 1 || sub.value[0].value.term === 'string')
|
||||||
|
&& operator.stringOrDestructuredPart
|
||||||
|
) {
|
||||||
this.subject = wrapDestructured(await Promise.all(
|
this.subject = wrapDestructured(await Promise.all(
|
||||||
unwrapDestructured(sub)
|
unwrapDestructured(sub)
|
||||||
.map(async part => ({
|
.map(async part => ({
|
||||||
...part,
|
...part,
|
||||||
value: await operator.stringOrDestructuredPart!(part.value),
|
value: wrapString(await operator.stringOrDestructuredPart!(unwrapString(part.value))),
|
||||||
}))))
|
}))))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -245,7 +306,7 @@ export class ExecutionContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('(todo: better error) Cannot replace subject: could not find an appropriate operation for the term type of the current subject')
|
throw new TermOperationError(`This operation does not apply to the subject. The subject must be: ${getTermOperatorInputDisplayList(operator).join('|')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(term: StrTerm): StrRVal|undefined {
|
resolve(term: StrTerm): StrRVal|undefined {
|
||||||
@@ -263,7 +324,8 @@ export class ExecutionContext {
|
|||||||
resolveRequired(term: StrTerm): StrRVal {
|
resolveRequired(term: StrTerm): StrRVal {
|
||||||
const rval = this.resolve(term)
|
const rval = this.resolve(term)
|
||||||
if ( !rval ) {
|
if ( !rval ) {
|
||||||
throw new Error('FIXME: undefined term')
|
const display = isStrLVal(term) ? term.name : '(unknown)'
|
||||||
|
throw new UndefinedTermError(`Could not find undefined term: ${display}`)
|
||||||
}
|
}
|
||||||
return rval
|
return rval
|
||||||
}
|
}
|
||||||
@@ -420,7 +482,19 @@ export class Executor extends BehaviorSubject<StrVM> implements LifecycleAware{
|
|||||||
constructor(private output: OutputManager, parser?: Parser, private input?: Input) {
|
constructor(private output: OutputManager, parser?: Parser, private input?: Input) {
|
||||||
super()
|
super()
|
||||||
this.logger = log.getStreamLogger('executor')
|
this.logger = log.getStreamLogger('executor')
|
||||||
parser?.subscribe(exec => this.handleExecutable(exec))
|
parser?.subscribe({
|
||||||
|
next: exec => this.handleExecutable(exec),
|
||||||
|
error: error => this.handleExecutionError(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleExecutionError(error: Error) {
|
||||||
|
if ( error instanceof ExecutionError || error instanceof TypeError ) {
|
||||||
|
this.logger.error(`(${error.constructor.name}) ${error.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
adoptLifecycle(lifecycle: Lifecycle): void {
|
adoptLifecycle(lifecycle: Lifecycle): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user