diff --git a/src/lib/ts/_entry.ts b/src/lib/ts/_entry.ts index 4d5a35b..f04710e 100644 --- a/src/lib/ts/_entry.ts +++ b/src/lib/ts/_entry.ts @@ -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 = {}; @@ -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) { diff --git a/src/lib/ts/map.ts b/src/lib/ts/map.ts new file mode 100644 index 0000000..4e22f33 --- /dev/null +++ b/src/lib/ts/map.ts @@ -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, + ) { } + + 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(); + + 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; + }; + } +} diff --git a/src/main/java/me/topchetoeu/jscript/common/mapping/FunctionMap.java b/src/main/java/me/topchetoeu/jscript/common/mapping/FunctionMap.java index 3f6aa7e..3102bb6 100644 --- a/src/main/java/me/topchetoeu/jscript/common/mapping/FunctionMap.java +++ b/src/main/java/me/topchetoeu/jscript/common/mapping/FunctionMap.java @@ -56,10 +56,14 @@ public class FunctionMap { var newBreakpoints = new HashMap(); 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(); diff --git a/src/main/java/me/topchetoeu/jscript/repl/mapping/NativeMapper.java b/src/main/java/me/topchetoeu/jscript/repl/mapping/NativeMapper.java index 6dc9b23..e6cb82b 100644 --- a/src/main/java/me/topchetoeu/jscript/repl/mapping/NativeMapper.java +++ b/src/main/java/me/topchetoeu/jscript/repl/mapping/NativeMapper.java @@ -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"); }; } } diff --git a/src/main/java/me/topchetoeu/jscript/repl/mapping/SourceMap.java b/src/main/java/me/topchetoeu/jscript/repl/mapping/SourceMap.java deleted file mode 100644 index 00c847d..0000000 --- a/src/main/java/me/topchetoeu/jscript/repl/mapping/SourceMap.java +++ /dev/null @@ -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 origToComp = new TreeMap<>(); - private final TreeMap 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 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 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 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(); - // } -} \ No newline at end of file diff --git a/src/main/java/me/topchetoeu/jscript/repl/mapping/VLQ.java b/src/main/java/me/topchetoeu/jscript/repl/mapping/VLQ.java deleted file mode 100644 index 50d3c71..0000000 --- a/src/main/java/me/topchetoeu/jscript/repl/mapping/VLQ.java +++ /dev/null @@ -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 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(); - - 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(); - - for (var line : val.split(";", -1)) { - var elements = new ArrayList(); - for (var el : line.split(",", -1)) { - elements.add(decode(el)); - } - lines.add(elements.toArray(new long[0][])); - } - - return lines.toArray(new long[0][][]); - } -} \ No newline at end of file