| import {_, nil, Code, Name} from "./code"
|
|
|
| interface NameGroup {
|
| prefix: string
|
| index: number
|
| }
|
|
|
| export interface NameValue {
|
| ref: ValueReference
|
| key?: unknown
|
| code?: Code
|
| }
|
|
|
| export type ValueReference = unknown
|
|
|
| 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
|
| }
|
| }
|
|
|