refactor: implement source mapping in javascript, instead of java

This commit is contained in:
TopchetoEU 2024-12-27 19:11:32 +02:00
parent 398d88c0a5
commit 8a34db833c
Signed by: topchetoeu
GPG Key ID: 6531B8583E5F6ED4
6 changed files with 200 additions and 231 deletions

View File

@ -1,15 +1,13 @@
import { createDocumentRegistry, createLanguageService, ModuleKind, ScriptSnapshot, ScriptTarget, type Diagnostic, type CompilerOptions, type IScriptSnapshot, flattenDiagnosticMessageText, CompilerHost, LanguageService } from "typescript";
import { SourceMap } from "./map.ts";
declare function getResource(name: string): string | undefined;
declare function print(...args: any[]): void;
declare function register(factory: CompilerFactory): void;
declare function parseVLQ(compiled: string, original: string, map: string): (loc: Location) => Location;
declare function chainMaps(...mappers: ((loc: Location) => Location)[]): (loc: Location) => Location;
declare function registerSource(filename: string, src: string): void;
type Location = readonly [filename: string, line: number, col: number];
type CompilerFactory = (next: Compiler) => Compiler;
type Compiler = (filename: string, src: string, mapper: (loc: Location) => Location) => Function;
type Compiler = (filename: string, src: string, mapper: SourceMap) => Function;
const resources: Record<string, string | undefined> = {};
@ -108,11 +106,15 @@ register(next => {
}
const rawMap = JSON.parse(outputs["/src.js.map"]);
const map = parseVLQ(filename, filename, rawMap.mappings);
const map = SourceMap.parse({
file: "ts-internal://" + filename,
mappings: rawMap.mappings,
sources: [filename],
});
const result = outputs["/src.js"];
const declaration = outputs["/src.d.ts"];
const compiled = next(filename, result, chainMaps(prevMap, map));
const compiled = next("ts-internal://" + filename, result, SourceMap.chain(prevMap, map));
registerSource(filename, code);
return function (this: any) {

178
src/lib/ts/map.ts Normal file
View File

@ -0,0 +1,178 @@
const map: number[] = [];
let j = 0;
for (let i = 65; i <= 90; i++) map[i] = j++;
for (let i = 97; i <= 122; i++) map[i] = j++;
map[43] = j++;
map[47] = j++;
export type Location = readonly [file: string, line: number, start: number];
export function decodeVLQ(val: string): number[][][] {
const lines: number[][][] = [];
for (const line of val.split(";", -1)) {
const elements: number[][] = [];
for (const el of line.split(",", -1)) {
if (el.length === 0) elements.push([]);
else {
const list: number[] = [];
for (let i = 0; i < el.length;) {
let sign = 1;
let curr = map[el.charCodeAt(i++)];
let cont = (curr & 0x20) === 0x20;
if ((curr & 1) === 1) sign = -1;
let res = (curr & 0b11110) >> 1;
let n = 4;
for (; i < el.length && cont;) {
curr = map[el.charCodeAt(i++)];
cont = (curr & 0x20) == 0x20;
res |= (curr & 0b11111) << n;
n += 5;
if (!cont) break;
}
list.push(res * sign);
}
elements.push(list);
}
}
lines.push(elements);
}
return lines;
}
export namespace Location {
export function compare(a: Location, b: Location) {
const { 0: filenameA, 1: lineA, 2: startA } = a;
const { 0: filenameB, 1: lineB, 2: startB } = b;
if (filenameA < filenameB) return -1;
if (filenameA > filenameB) return 1;
const lineI = lineA - lineB;
if (lineI !== 0) return lineI;
return startA - startB;
}
export function comparePoints(a: Location, b: Location) {
const { 1: lineA, 2: startA } = a;
const { 1: lineB, 2: startB } = b;
const lineI = lineA - lineB;
if (lineI !== 0) return lineI;
return startA - startB;
}
}
export interface SourceMap {
(loc: Location): Location | undefined;
}
export class VLQSourceMap {
public constructor(
public readonly array: Map<string, [start: number, dst: Location][][]>,
) { }
public converter() {
return (src: Location) => {
const file = this.array.get(src[0]);
if (file == null) return src;
const line = file[src[1]];
if (line == null || line.length === 0) return undefined;
let a = 0;
let b = line.length;
while (true) {
const done = b - a <= 1;
const mid = (a + b) >> 1;
const el = line[mid];
const cmp = el[0] - src[1];
if (cmp < 0) {
if (done) {
if (b >= line.length) return undefined;
break;
}
a = mid;
}
else if (cmp > 0) {
if (done) {
if (a <= 0) return undefined;
break;
}
b = mid;
}
else return el[1];
}
return line[b][1];
};
}
public static parseVLQ(compiled: string, filenames: string[], raw: string): VLQSourceMap {
const mapping = decodeVLQ(raw);
const res = new Map<string, [start: number, dst: Location][][]>();
let originalRow = 0;
let originalCol = 0;
let originalFile = 0;
for (let compiledRow = 0; compiledRow < mapping.length; compiledRow++) {
let compiledCol = 0;
for (const rawSeg of mapping[compiledRow]) {
compiledCol += rawSeg.length > 0 ? rawSeg[0] : 0;
originalFile += rawSeg.length > 1 ? rawSeg[1] : 0;
originalRow += rawSeg.length > 2 ? rawSeg[2] : 0;
originalCol += rawSeg.length > 3 ? rawSeg[3] : 0;
let file = res.get(compiled);
if (file == null) res.set(compiled, file = []);
const line = file[compiledRow] ??= [];
line[line.length] = [compiledCol, [filenames[originalFile], originalRow, originalCol]];
}
}
return new VLQSourceMap(res);
}
public static parse(raw: string | { file: string, mappings: string, sources: string[] }) {
if (typeof raw === "string") raw = JSON.parse(raw) as { file: string, mappings: string, sources: string[] };
const compiled = raw.file;
const mapping = raw.mappings;
let filenames = raw.sources;
if (filenames.length === 0 || filenames.length === 1 && filenames[0] === "") filenames = [compiled];
return this.parseVLQ(compiled, filenames, mapping);
}
}
export namespace SourceMap {
export function parse(raw: string | { file: string, mappings: string, sources: string[] }) {
return VLQSourceMap.parse(raw).converter();
}
export function chain(...maps: SourceMap[]): SourceMap {
return loc => {
for (const el of maps) {
const tmp = el(loc);
if (tmp == null) return undefined;
else loc = tmp;
}
return loc;
};
}
}

View File

@ -56,10 +56,14 @@ public class FunctionMap {
var newBreakpoints = new HashMap<Location, BreakpointType>();
for (var key : sourceMap.keySet()) {
newSourceMaps.put(key, mapper.apply(sourceMap.get(key)));
var mapped = mapper.apply(sourceMap.get(key));
if (mapped == null) continue;
newSourceMaps.put(key, mapped);
}
for (var key : breakpoints.keySet()) {
newBreakpoints.put(mapper.apply(key), breakpoints.get(key));
var mapped = mapper.apply(key);
if (mapped == null) continue;
newBreakpoints.put(mapped, breakpoints.get(key));
}
sourceMap.clear();

View File

@ -25,6 +25,7 @@ public class NativeMapper extends FunctionValue {
);
var res = mapper.apply(loc);
if (res == null) return Value.UNDEFINED;
return new ArrayValue(
StringValue.of(res.filename().toString()),
@ -52,12 +53,14 @@ public class NativeMapper extends FunctionValue {
NumberValue.of(loc.start())
);
var rawRes = (ArrayLikeValue)func.apply(env, Value.UNDEFINED, rawLoc);
return Location.of(
Filename.parse(rawRes.get(0).toString(env)),
rawRes.get(1).toNumber(env).getInt(),
rawRes.get(2).toNumber(env).getInt()
var rawRes = func.apply(env, Value.UNDEFINED, rawLoc);
if (rawRes instanceof ArrayLikeValue arr) return Location.of(
Filename.parse(arr.get(0).toString(env)),
arr.get(1).toNumber(env).getInt(),
arr.get(2).toNumber(env).getInt()
);
else if (rawRes == Value.UNDEFINED || rawRes == Value.NULL) return null;
else throw EngineException.ofType("Location must be an array, null or undefined");
};
}
}

View File

@ -1,123 +0,0 @@
package me.topchetoeu.jscript.repl.mapping;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Collectors;
import me.topchetoeu.jscript.common.json.JSON;
import me.topchetoeu.jscript.common.parsing.Filename;
import me.topchetoeu.jscript.common.parsing.Location;
public class SourceMap {
private final TreeMap<Long, Long> origToComp = new TreeMap<>();
private final TreeMap<Long, Long> compToOrig = new TreeMap<>();
private final Filename compiled, original;
public Location toCompiled(Location loc) { return convert(original, compiled, loc, origToComp); }
public Location toOriginal(Location loc) { return convert(compiled, original, loc, compToOrig); }
private void add(long orig, long comp) {
var a = origToComp.remove(orig);
var b = compToOrig.remove(comp);
if (b != null) origToComp.remove(b);
if (a != null) compToOrig.remove(a);
origToComp.put(orig, comp);
compToOrig.put(comp, orig);
}
public SourceMap apply(SourceMap map) {
var res = new SourceMap(map.compiled, map.original);
for (var el : new ArrayList<>(origToComp.entrySet())) {
var mapped = convert(el.getValue(), map.origToComp);
res.origToComp.put(el.getKey(), mapped);
}
for (var el : new ArrayList<>(compToOrig.entrySet())) {
var mapped = convert(el.getKey(), map.compToOrig);
res.compToOrig.put(mapped, el.getValue());
res.add(el.getValue(), mapped);
}
return res;
}
public SourceMap clone() {
var res = new SourceMap(this.compiled, this.original);
res.origToComp.putAll(this.origToComp);
res.compToOrig.putAll(this.compToOrig);
return res;
}
public SourceMap(Filename compiled, Filename original) {
this.compiled = compiled;
this.original = original;
}
public static SourceMap parse(Filename compiled, Filename original, String raw) {
var mapping = VLQ.decodeMapping(raw);
var res = new SourceMap(compiled, original);
var compRow = 0l;
var compCol = 0l;
for (var origRow = 0; origRow < mapping.length; origRow++) {
var origCol = 0;
for (var rawSeg : mapping[origRow]) {
if (rawSeg.length > 1 && rawSeg[1] != 0) throw new IllegalArgumentException("Source mapping is to more than one files.");
origCol += rawSeg.length > 0 ? rawSeg[0] : 0;
compRow += rawSeg.length > 2 ? rawSeg[2] : 0;
compCol += rawSeg.length > 3 ? rawSeg[3] : 0;
var compPacked = ((long)compRow << 32) | compCol;
var origPacked = ((long)origRow << 32) | origCol;
res.add(origPacked, compPacked);
}
}
return res;
}
public static List<String> getSources(String raw) {
var json = JSON.parse(null, raw).map();
return json
.list("sourcesContent")
.stream()
.map(v -> v.string())
.collect(Collectors.toList());
}
public static SourceMap chain(SourceMap ...maps) {
if (maps.length == 0) return null;
var res = maps[0];
for (var i = 1; i < maps.length; i++) res = res.apply(maps[i]);
return res;
}
private static Long convert(long packed, TreeMap<Long, Long> map) {
if (map.containsKey(packed)) return map.get(packed);
var key = map.floorKey(packed);
if (key == null) return null;
else return map.get(key);
}
private static Location convert(Filename src, Filename dst, Location loc, TreeMap<Long, Long> map) {
if (!loc.filename().equals(src)) return loc;
var packed = ((loc.line()) << 32) | (loc.start());
var resPacked = convert(packed, map);
if (resPacked == null) return null;
else return Location.of(dst, (int)(resPacked >> 32), (int)(resPacked & 0xFFFF));
}
// public static SourceMap of(String filename, String raw) {
// var json = JSON.parse(Filename.parse(filename), raw);
// return new SourceMap();
// }
}

View File

@ -1,95 +0,0 @@
package me.topchetoeu.jscript.repl.mapping;
import java.util.ArrayList;
import java.util.List;
public class VLQ {
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static long[] toArray(List<Long> list) {
var arr = new long[list.size()];
for (var i = 0; i < list.size(); i++) arr[i] = list.get(i);
return arr;
}
public static String encode(long... arr) {
var raw = new StringBuilder();
for (var data : arr) {
var b = data < 0 ? 1 : 0;
data = Math.abs(data);
b |= (int)(data & 0b1111) << 1;
data >>= 4;
b |= data > 0 ? 0x20 : 0;;
raw.append(ALPHABET.charAt(b));
while (data > 0) {
b = (int)(data & 0b11111);
data >>= 5;
b |= data > 0 ? 0x20 : 0;
raw.append(ALPHABET.charAt(b));
}
}
return raw.toString();
}
public static long[] decode(String val) {
if (val.length() == 0) return new long[0];
var list = new ArrayList<Long>();
for (var i = 0; i < val.length();) {
var sign = 1;
var curr = ALPHABET.indexOf(val.charAt(i++));
var cont = (curr & 0x20) == 0x20;
if ((curr & 1) == 1) sign = -1;
long res = (curr & 0b11110) >> 1;
var n = 4;
for (; i < val.length() && cont;) {
curr = ALPHABET.indexOf(val.charAt(i++));
cont = (curr & 0x20) == 0x20;
res |= (curr & 0b11111) << n;
n += 5;
if (!cont) break;
}
list.add(res * sign);
}
return toArray(list);
}
public static String encodeMapping(long[][][] arr) {
var res = new StringBuilder();
var semicolon = false;
for (var line : arr) {
var comma = false;
if (semicolon) res.append(";");
semicolon = true;
for (var el : line) {
if (comma) res.append(",");
comma = true;
res.append(encode(el));
}
}
return res.toString();
}
public static long[][][] decodeMapping(String val) {
var lines = new ArrayList<long[][]>();
for (var line : val.split(";", -1)) {
var elements = new ArrayList<long[]>();
for (var el : line.split(",", -1)) {
elements.add(decode(el));
}
lines.add(elements.toArray(new long[0][]));
}
return lines.toArray(new long[0][][]);
}
}