215 lines
5.8 KiB
TypeScript
Executable file
215 lines
5.8 KiB
TypeScript
Executable file
import {_, nil, Code, Name} from "./code"
|
|
|
|
interface NameGroup {
|
|
prefix: string
|
|
index: number
|
|
}
|
|
|
|
export interface NameValue {
|
|
ref: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure
|
|
key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used
|
|
code?: Code // this is the code creating the value needed for standalone code wit_out closure - can be a primitive value, function or import (`require`)
|
|
}
|
|
|
|
export type ValueReference = unknown // possibly make CodeGen parameterized type on this type
|
|
|
|
class ValueError extends Error {
|
|
readonly value?: NameValue
|
|
constructor(name: ValueScopeName) {
|
|
super(`CodeGen: "code" for ${name} not defined`)
|
|
this.value = name.value
|
|
}
|
|
}
|
|
|
|
interface ScopeOptions {
|
|
prefixes?: Set<string>
|
|
parent?: Scope
|
|
}
|
|
|
|
interface ValueScopeOptions extends ScopeOptions {
|
|
scope: ScopeStore
|
|
es5?: boolean
|
|
lines?: boolean
|
|
}
|
|
|
|
export type ScopeStore = Record<string, ValueReference[] | undefined>
|
|
|
|
type ScopeValues = {
|
|
[Prefix in string]?: Map<unknown, ValueScopeName>
|
|
}
|
|
|
|
export type ScopeValueSets = {
|
|
[Prefix in string]?: Set<ValueScopeName>
|
|
}
|
|
|
|
export enum UsedValueState {
|
|
Started,
|
|
Completed,
|
|
}
|
|
|
|
export type UsedScopeValues = {
|
|
[Prefix in string]?: Map<ValueScopeName, UsedValueState | undefined>
|
|
}
|
|
|
|
export const varKinds = {
|
|
const: new Name("const"),
|
|
let: new Name("let"),
|
|
var: new Name("var"),
|
|
}
|
|
|
|
export class Scope {
|
|
protected readonly _names: {[Prefix in string]?: NameGroup} = {}
|
|
protected readonly _prefixes?: Set<string>
|
|
protected readonly _parent?: Scope
|
|
|
|
constructor({prefixes, parent}: ScopeOptions = {}) {
|
|
this._prefixes = prefixes
|
|
this._parent = parent
|
|
}
|
|
|
|
toName(nameOrPrefix: Name | string): Name {
|
|
return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix)
|
|
}
|
|
|
|
name(prefix: string): Name {
|
|
return new Name(this._newName(prefix))
|
|
}
|
|
|
|
protected _newName(prefix: string): string {
|
|
const ng = this._names[prefix] || this._nameGroup(prefix)
|
|
return `${prefix}${ng.index++}`
|
|
}
|
|
|
|
private _nameGroup(prefix: string): NameGroup {
|
|
if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes.has(prefix))) {
|
|
throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`)
|
|
}
|
|
return (this._names[prefix] = {prefix, index: 0})
|
|
}
|
|
}
|
|
|
|
interface ScopePath {
|
|
property: string
|
|
itemIndex: number
|
|
}
|
|
|
|
export class ValueScopeName extends Name {
|
|
readonly prefix: string
|
|
value?: NameValue
|
|
scopePath?: Code
|
|
|
|
constructor(prefix: string, nameStr: string) {
|
|
super(nameStr)
|
|
this.prefix = prefix
|
|
}
|
|
|
|
setValue(value: NameValue, {property, itemIndex}: ScopePath): void {
|
|
this.value = value
|
|
this.scopePath = _`.${new Name(property)}[${itemIndex}]`
|
|
}
|
|
}
|
|
|
|
interface VSOptions extends ValueScopeOptions {
|
|
_n: Code
|
|
}
|
|
|
|
const line = _`\n`
|
|
|
|
export class ValueScope extends Scope {
|
|
protected readonly _values: ScopeValues = {}
|
|
protected readonly _scope: ScopeStore
|
|
readonly opts: VSOptions
|
|
|
|
constructor(opts: ValueScopeOptions) {
|
|
super(opts)
|
|
this._scope = opts.scope
|
|
this.opts = {...opts, _n: opts.lines ? line : nil}
|
|
}
|
|
|
|
get(): ScopeStore {
|
|
return this._scope
|
|
}
|
|
|
|
name(prefix: string): ValueScopeName {
|
|
return new ValueScopeName(prefix, this._newName(prefix))
|
|
}
|
|
|
|
value(nameOrPrefix: ValueScopeName | string, value: NameValue): ValueScopeName {
|
|
if (value.ref === undefined) throw new Error("CodeGen: ref must be passed in value")
|
|
const name = this.toName(nameOrPrefix) as ValueScopeName
|
|
const {prefix} = name
|
|
const valueKey = value.key ?? value.ref
|
|
let vs = this._values[prefix]
|
|
if (vs) {
|
|
const _name = vs.get(valueKey)
|
|
if (_name) return _name
|
|
} else {
|
|
vs = this._values[prefix] = new Map()
|
|
}
|
|
vs.set(valueKey, name)
|
|
|
|
const s = this._scope[prefix] || (this._scope[prefix] = [])
|
|
const itemIndex = s.length
|
|
s[itemIndex] = value.ref
|
|
name.setValue(value, {property: prefix, itemIndex})
|
|
return name
|
|
}
|
|
|
|
getValue(prefix: string, keyOrRef: unknown): ValueScopeName | undefined {
|
|
const vs = this._values[prefix]
|
|
if (!vs) return
|
|
return vs.get(keyOrRef)
|
|
}
|
|
|
|
scopeRefs(scopeName: Name, values: ScopeValues | ScopeValueSets = this._values): Code {
|
|
return this._reduceValues(values, (name: ValueScopeName) => {
|
|
if (name.scopePath === undefined) throw new Error(`CodeGen: name "${name}" has no value`)
|
|
return _`${scopeName}${name.scopePath}`
|
|
})
|
|
}
|
|
|
|
scopeCode(
|
|
values: ScopeValues | ScopeValueSets = this._values,
|
|
usedValues?: UsedScopeValues,
|
|
getCode?: (n: ValueScopeName) => Code | undefined
|
|
): Code {
|
|
return this._reduceValues(
|
|
values,
|
|
(name: ValueScopeName) => {
|
|
if (name.value === undefined) throw new Error(`CodeGen: name "${name}" has no value`)
|
|
return name.value.code
|
|
},
|
|
usedValues,
|
|
getCode
|
|
)
|
|
}
|
|
|
|
private _reduceValues(
|
|
values: ScopeValues | ScopeValueSets,
|
|
valueCode: (n: ValueScopeName) => Code | undefined,
|
|
usedValues: UsedScopeValues = {},
|
|
getCode?: (n: ValueScopeName) => Code | undefined
|
|
): Code {
|
|
let code: Code = nil
|
|
for (const prefix in values) {
|
|
const vs = values[prefix]
|
|
if (!vs) continue
|
|
const nameSet = (usedValues[prefix] = usedValues[prefix] || new Map())
|
|
vs.forEach((name: ValueScopeName) => {
|
|
if (nameSet.has(name)) return
|
|
nameSet.set(name, UsedValueState.Started)
|
|
let c = valueCode(name)
|
|
if (c) {
|
|
const def = this.opts.es5 ? varKinds.var : varKinds.const
|
|
code = _`${code}${def} ${name} = ${c};${this.opts._n}`
|
|
} else if ((c = getCode?.(name))) {
|
|
code = _`${code}${c}${this.opts._n}`
|
|
} else {
|
|
throw new ValueError(name)
|
|
}
|
|
nameSet.set(name, UsedValueState.Completed)
|
|
})
|
|
}
|
|
return code
|
|
}
|
|
}
|