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
			| 
											3 years ago
										 | 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 | ||
|  |   } | ||
|  | } |