JavaScript: LZ4圧縮補完計画

LZ4と呼ばれる圧縮法を改良しようという企画。ほぼ圧縮率を重点的に。圧縮速度は控え目であるが…気にしてはいけない。

## 改良法
巨大一致と巨大不一致の符号化を工夫する。本家は巨大な値`n`を `n/255` bytes程度出力するが、私家版は `log256(n)` bytes程度の出力で収まる。スン、バラスィー。などと自画自賛しているようではprogrammerとして終わっている。
それはともかく、最長一致系列は**全ての地点でくまなくもれなく**徹底的に調べ上げ、符号化費用対効果を最適化しまくった挙句の果てに、何とか全てを出力する事に成功する。
言うまでもない事だが、本家との互換性は概ね皆無である。

## 実装編
```js
/*compress
@In: input(Array / Uint8Array)
@done: call back of last process
	done(A,a,z)
	@A: compressed array of input
	@a: input size
	@z: compressed size
@rate: call back of progress
	rate(a,z)
	@a: current position
	@z: last position
@return:
	call with await: compressed array of @In
	without await: Promise object
*/
async function LZ4enc(In,done=a=>a,rate){
	var m,n=In.length,o=0,p=0;
	const W=1<<16,M=W-1,Out=[],
		// for match finder
		Hash=new Int32Array(1<<18).fill(-1), Tree=new Int32Array(W*2),
		// for optimal parser
		Cost=new Uint32Array(n*2), Len=Cost.subarray(n), Dist=new Uint16Array(n),
		pass=rate?32767:-1,fn=b=>wait0(c=>b(rate(p,n)));
	// Pass 1: Find all matches
	for(;p<n;p&pass||await new Promise(fn)){
		let v=0,m=n-p,d;
		if(m>3){// get matches used binary tree match finder
			let r=p&M,l=r|W,b=0,c=0,w=p<W?-1:p-W,
				h=(In[p]|In[p+1]<<8|In[p+2]<<16|In[p+3]<<24)*2097143>>>14, s=Hash[h];
			for(Hash[h]=p;s>w;){
				let i=b<c?b:c;
				if(In[s+i]===In[p+i]){
					for(;++i<m&&In[s+i]===In[p+i];);
					if(i>v){
						v=i;d=p-s;
						if(i===m)break
					}
				}
				if(In[s+i]<In[p+i])
					Tree[r]=s,s=Tree[r=s&M|W],c=i;
				else Tree[l]=s,s=Tree[l=s&M],b=i}
			Tree[l]=Tree[r]=-1
		}
		Len[p]=v;Dist[p++]=d
	}
	// Pass 2: Build the shortest path
	for(let s=15;--p>0;){
		let c=Cost[p+1]+1;--s||(s=255,++c);
		if(Len[p]>3){
			Cost[p]=1<<30;
			for(let l=Len[p];l>3;--l){
				let d=Cost[p+l]+3,i=l-4;
				if(i>14)++d,i-=15,i>252&&(i-=253,d+=i>>16?3:i>>8?2:1);
				if(d<Cost[p])Cost[p]=d,Len[p]=l
			}
			if(c<Cost[p])Cost[p]=c,Len[p]=0;
			else s=15
		}else Cost[p]=c
	}
	// Pass 3: Output the codes
	for(m=p=0;p<n;)
		if(Len[p]>3){
			let l=Len[p]-4,d=l<15?l:15;
			if(m<p){
				let r=p-m;
				if(r>14){
					Out[o++]=240+d;
					if((r-=15)<253)Out[o++]=r;
					else for(Out[o++]=(r-=253)<256?253:r<65536?254:255;Out[o++]=r&255,r>>=8;);
				}else Out[o++]=r<<4|d;
				for(;m<p;)Out[o++]=In[m++]
			}else Out[o++]=d;
			Out[o++]=Dist[p]>>8;Out[o++]=Dist[p]&255;
			if(l>14){
				if((l-=15)<253)Out[o++]=l;
				else for(Out[o++]=(l-=253)<256?253:l<65536?254:255;Out[o++]=l&255,l>>=8;);
			}
			m=p+=Len[p]
		}else++p;
	if(m<p){
		let r=p-m;
		if(r>14){
			Out[o++]=240;
			if((r-=15)<253)Out[o++]=r;
			else for(Out[o++]=(r-=253)<256?253:r<65536?254:255;Out[o++]=r&255,r>>=8;);
		}else Out[o++]=r<<4;
		for(;m<p;)Out[o++]=In[m++]
	}done&&done(Out,n,o);return Out
}
/*decompress
@In: input(Array / Uint8Array)
@done: call back of last process
	done(A,a,z)
	@A: decompressed array of @In
	@a: last position
	@z: decompressed size
@rate: call back of progress
	rate(a,z)
	@a: current position
	@z: last position
@return:
	call with await: decompressed array of @In
	without await: Promise object
*/
async function LZ4dec(In,done=a=>a,rate){
	var a=0,b,c,size=In.length,l,o=0,p;
	const MINMATCH=4,ML_BITS=4,ML_MASK=(1<<ML_BITS)-1,RUN_BITS=8-ML_BITS,RUN_MASK=(1<<RUN_BITS)-1,
		Out=[],pass=rate?32767:-1,fn=b=>wait0(c=>b(rate(a,size)));
	for(;;o&pass||await new Promise(fn)){
		c=In[a++];
		if(l=c>>ML_BITS){
			if(l===RUN_MASK)
				if((b=In[a++])<253)l+=b;
				else for(l+=253,b-=252,p=0;l+=In[a++]<<p,--b;)p+=8;
			for(;l--;)Out[o++]=In[a++]
		}
		p=o-(In[a++]<<8|In[a++]);
		if(a>size)return done(Out,size,o),Out;
		if((c&=ML_MASK)===ML_MASK)
				if((b=In[a++])<253)c+=b;
				else for(c+=253,l=0;c+=In[a++]<<l,--b>252;)l+=8;
		for(c+=MINMATCH;c--;)Out[o++]=Out[p++]
	}
}
// setImmediateもどき
(function(a){
	var F=[],hit="\0";
	a.wait0=function(fn){
		F[F.length]=fn;
		a.postMessage(hit,"*")
	};
	a.onmessage=function(e){
		if(e.source===a&&e.data===hit)
			e.stopPropagation(),
			F[0]&&F.shift()()
	}
})(this)
```
```js:使用例
(async()=>{
let A=Array.from("That that is is that that is not is not is that it it is",a=>a.charCodeAt());
let rate=(a,b)=>{console.log((a/b*1e4|0)/100,"%")}, done=(a,b,c)=>{console.log(b,"->",c,a)};
let e=await LZ4enc(A,done,rate), d=await LZ4dec(e,done,rate);
console.log(e.length, String.fromCharCode(...d))

// without await
LZ4enc(A).then(a=>LZ4dec(a).then(b=>console.log(a.length,b.length,b)))
})()
```
file圧縮伸長実験
<p class=codepen data-height="300" data-default-tab="result" data-slug-hash="KKLOEVO" data-pen-title="LZ4 variant compressor" data-user="xezz" style="height:300px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border:2px solid;margin:1em 0;padding:1em"> <span>See the Pen <a href="https://codepen.io/xezz/pen/KKLOEVO">LZ4 variant compressor</a> by xezz (<a href="https://codepen.io/xezz">@xezz</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## 参考文献様
[本家](https://github.com/lz4/lz4)