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 parent?: Scope } interface ValueScopeOptions extends ScopeOptions { scope: ScopeStore es5?: boolean lines?: boolean } export type ScopeStore = Record type ScopeValues = { [Prefix in string]?: Map } export type ScopeValueSets = { [Prefix in string]?: Set } export enum UsedValueState { Started, Completed, } export type UsedScopeValues = { [Prefix in string]?: Map } 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 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 } }