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
 | |
|   }
 | |
| }
 |