You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							216 lines
						
					
					
						
							5.8 KiB
						
					
					
				
			
		
		
	
	
							216 lines
						
					
					
						
							5.8 KiB
						
					
					
				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
 | 
						|
  }
 | 
						|
}
 |