Spaces:
Running
Running
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 | |
} | |
} | |