Newt - a dependent typed programming language

Wire web playground to LSP code

+185 -103
+6 -3
Makefile
··· 60 60 lsp.js: ${SRCS} 61 61 node newt.js src/LSP.newt -o lsp.js 62 62 63 - newt-vscode-lsp/src/newt.js: ${SRCS} 64 - node newt.js src/LSP.newt -o $@ 63 + newt-vscode-lsp/src/newt.js: lsp.js 64 + cp lsp.js $@ 65 + 66 + playground/src/newt.js: lsp.js 67 + cp lsp.js $@ 65 68 66 69 newt-vscode-lsp/dist/lsp.js: newt-vscode-lsp/src/lsp.ts newt-vscode-lsp/src/newt.js 67 70 (cd newt-vscode-lsp && node esbuild.js) 68 71 69 - lsp: newt-vscode-lsp/dist/lsp.js 72 + lsp: newt-vscode-lsp/dist/lsp.js playground/src/newt.js 70 73
+3 -2
playground/build
··· 1 1 #!/bin/sh 2 2 mkdir -p public 3 + echo copy newt 4 + #cp ../lsp.js src/newt.js 5 + (cd .. && make lsp) 3 6 echo build newt worker 4 7 esbuild src/worker.ts --bundle --format=esm --platform=browser > public/worker.js 5 8 esbuild src/frame.ts --bundle --format=esm --platform=browser > public/frame.js 6 - echo copy newt 7 - cp ../newt.js src/newt.js 8 9 cp -r static/* public 9 10 (cd samples && zip -r ../public/files.zip .)
+21 -24
playground/src/cmeditor.ts
··· 234 234 }); 235 235 }), 236 236 this.theme.of(EditorView.baseTheme({})), 237 - hoverTooltip((view, pos) => { 237 + hoverTooltip(async (view, pos) => { 238 238 let cursor = this.view.state.doc.lineAt(pos); 239 - let line = cursor.number; 240 - let range = this.view.state.wordAt(pos); 241 - console.log(range); 242 - if (range) { 243 - let col = range.from - cursor.from; 244 - let word = this.view.state.doc.sliceString(range.from, range.to); 245 - let entry = this.delegate.getEntry(word, line, col); 246 - if (!entry) 247 - entry = this.delegate.getEntry("_" + word + "_", line, col); 248 - console.log("entry for", word, "is", entry); 249 - if (entry) { 250 - let rval: Tooltip = { 251 - pos: range.head, 252 - above: true, 253 - create: () => { 254 - let dom = document.createElement("div"); 255 - dom.className = "tooltip"; 256 - dom.textContent = entry.type; 257 - return { dom }; 258 - }, 259 - }; 260 - return rval; 261 - } 239 + let line = cursor.number - 1; 240 + let col = pos - cursor.from; 241 + // let range = this.view.state.wordAt(pos); 242 + console.log('getting hover for ',line, col); 243 + let entry = await this.delegate.getEntry('', line, col) 244 + 245 + if (entry) { 246 + let rval: Tooltip = { 247 + // TODO pull in position from LSP (currently it only has the jump-to FC) 248 + pos, 249 + above: true, 250 + create: () => { 251 + let dom = document.createElement("div"); 252 + dom.className = "tooltip"; 253 + dom.textContent = entry.info; 254 + return { dom }; 255 + }, 256 + }; 257 + return rval; 262 258 } 259 + 263 260 // we'll iterate the syntax tree for word. 264 261 // let entry = delegate.getEntry(word, line, col) 265 262 return null;
+52 -7
playground/src/ipc.ts
··· 1 + 2 + //// Copy of LSP types 3 + 4 + export interface Location { uri: string; range: Range; } 5 + export interface Position { line: number; character: number; } 6 + export interface Range { start: Position; end: Position; } 7 + export interface HoverResult { info: string; location: Location; } 8 + export interface TextEdit { range: Range; newText: string; } 9 + export type DiagnosticSeverity = 1 | 2 | 3 | 4 10 + export interface DiagnosticRelatedInformation { location: Location; message: string; } 11 + export interface Diagnostic { 12 + range: Range 13 + message: string 14 + severity?: DiagnosticSeverity 15 + source?: string 16 + // we don't emit this yet, but I think we will 17 + relatedInformation?: DiagnosticRelatedInformation[] 18 + } 19 + 20 + export interface WorkspaceEdit { 21 + changes?: { 22 + [uri: string]: TextEdit[]; 23 + } 24 + } 25 + 26 + export interface CodeAction { 27 + title: string; 28 + edit?: WorkspaceEdit; 29 + } 30 + 31 + export interface BuildResult { 32 + diags: Diagnostic[] 33 + output: string 34 + } 35 + 36 + //// IPC Thinger 37 + 1 38 export type Result<A> = 2 39 | { status: "ok"; value: A } 3 40 | { status: "err"; error: string }; 4 41 5 42 export interface API { 6 - save(fileName: string, content: string): string; 7 - typeCheck(fileName: string): string; 8 - compile(fileName: string): string; 43 + // Invalidates stuff and writes to an internal cache that overlays the "filesystem" 44 + updateFile(fileName: string, content: string): unknown; 45 + // Run checking, return diagnostics 46 + typeCheck(fileName: string): BuildResult; 47 + // returns True if we need to recheck - usually for files invalidating other files 48 + // The playground rarely hits this situation at the moment 49 + hoverInfo(fileName: string, row: number, col: number): HoverResult | boolean | null; 50 + codeActionInfo(fileName: string, row: number, col: number): CodeAction[] | null; 51 + // we need to add this to the LSP build 52 + compile(fileName: string): string; 9 53 } 10 54 11 55 export interface Message<K extends keyof API> { ··· 14 58 args: Parameters<API[K]>; 15 59 } 16 60 17 - export interface ResponseMSG { 61 + export interface ResponseMSG<K extends keyof API> { 18 62 id: number; 19 - result: string; 63 + result: Awaited<ReturnType<API[K]>>; 20 64 } 21 65 22 66 type Suspense = { ··· 33 77 this._postMessage = <K extends keyof API>(msg: Message<K>) => 34 78 newtWorker.postMessage(msg); 35 79 // Safari/MobileSafari have small stacks in webworkers. 36 - if (navigator.vendor.includes("Apple")) { 80 + // But support for the frame needs to be fixed 81 + if (navigator.vendor.includes("Apple") && false) { 37 82 const workerFrame = document.createElement("iframe"); 38 83 workerFrame.src = "worker.html"; 39 84 workerFrame.style.display = "none"; ··· 46 91 } 47 92 // Need to handle messages from the other iframe too? Or at least ignore them. 48 93 } 49 - onmessage = (ev: MessageEvent<ResponseMSG>) => { 94 + onmessage = <K extends keyof API>(ev: MessageEvent<ResponseMSG<K>>) => { 50 95 console.log("GET", ev.data); 51 96 // Maybe key off of type 52 97 if (ev.data.id) {
+27 -17
playground/src/main.ts
··· 14 14 import { CMEditor } from "./cmeditor.ts"; 15 15 import { deflate } from "./deflate.ts"; 16 16 import { inflate } from "./inflate.ts"; 17 - import { IPC } from "./ipc.ts"; 17 + import { IPC, Position } from "./ipc.ts"; 18 18 import helpText from "./help.md?raw"; 19 19 import { basicSetup, EditorView } from "codemirror"; 20 20 import {Compartment, EditorState} from "@codemirror/state"; ··· 81 81 console.log("SEND TO", iframe.contentWindow); 82 82 const fileName = state.currentFile.value; 83 83 // maybe send fileName, src? 84 - await ipc.sendMessage("save", [fileName, src]); 84 + await ipc.sendMessage("updateFile", [fileName, src]); 85 85 let js = await ipc.sendMessage("compile", [fileName]); 86 86 state.javascript.value = bundle(js); 87 87 } ··· 210 210 initialValue: string; 211 211 } 212 212 const language: EditorDelegate = { 213 - getEntry(word, _row, _col) { 214 - return topData?.context.find((entry) => entry.name === word); 213 + async getEntry(word, row, col) { 214 + let fileName = state.currentFile.value 215 + let res = await ipc.sendMessage("hoverInfo", [fileName, row, col]) 216 + console.log('HOVER', res, 'for', row, col) 217 + if (res == true) return null 218 + return res || null 215 219 }, 216 220 onChange(_value) { 217 221 // we're using lint() now ··· 228 232 let module = src.match(/module\s+([^\s]+)/)?.[1]; 229 233 if (module) { 230 234 // This causes problems with stuff like aoc/... 231 - state.currentFile.value = module.replace(".", "/") + ".newt"; 235 + state.currentFile.value = './' + module.replace(".", "/") + ".newt"; 232 236 } 233 237 // This is a little flashy 234 238 // state.javascript.value = '' 235 239 let fileName = state.currentFile.value; 236 240 console.log("FN", fileName); 237 241 try { 238 - await ipc.sendMessage("save", [fileName, src]); 239 - let out = await ipc.sendMessage("typeCheck", [fileName]); 240 - setOutput(out); 241 - let markers = processOutput(out); 242 + await ipc.sendMessage("updateFile", [fileName, src]); 243 + let res = await ipc.sendMessage("typeCheck", [fileName]); 242 244 let diags: Diagnostic[] = []; 243 - for (let marker of markers) { 244 - let col = marker.startColumn; 245 + for (let marker of res.diags) { 246 + let {start,end} = marker.range 247 + let xlate = (pos: Position): number => 248 + view.state.doc.line(pos.line + 1).from + pos.character 245 249 246 - let line = view.state.doc.line(marker.startLineNumber); 247 - const pos = line.from + col - 1; 248 - let word = view.state.wordAt(pos); 250 + // TODO double check the last two are right 251 + const SEVERITY: Diagnostic["severity"][] = [ "error", "error", "warning", "info", "hint"] 252 + console.error({ 253 + from: xlate(start), 254 + to: xlate(end), 255 + severity: SEVERITY[marker.severity ?? 1], 256 + message: marker.message, 257 + }) 249 258 diags.push({ 250 - from: word?.from ?? pos, 251 - to: word?.to ?? pos + 1, 252 - severity: marker.severity, 259 + from: xlate(start), 260 + to: xlate(end), 261 + severity: SEVERITY[marker.severity ?? 1], 253 262 message: marker.message, 254 263 }); 255 264 } 265 + setOutput(res.output) 256 266 // less flashy version 257 267 ipc.sendMessage("compile", [fileName]).then(js => state.javascript.value = bundle(js)); 258 268 return diags;
+3 -4
playground/src/types.ts
··· 1 1 import { EditorView } from "codemirror"; 2 - import { linter, Diagnostic } from "@codemirror/lint"; 3 - 4 - 2 + import { Diagnostic } from "@codemirror/lint"; 3 + import { HoverResult } from "./ipc"; 5 4 6 5 export interface CompileReq { 7 6 id: string ··· 55 54 context: TopEntry[]; 56 55 } 57 56 export interface EditorDelegate { 58 - getEntry(word: string, row: number, col: number): TopEntry | undefined 57 + getEntry(word: string, row: number, col: number): Promise<HoverResult | null> 59 58 onChange(value: string): unknown 60 59 getFileName(): string 61 60 lint(view: EditorView): Promise<Diagnostic[]> | Diagnostic[]
+33 -35
playground/src/worker.ts
··· 1 1 import { shim } from "./emul"; 2 2 import { API, Message, ResponseMSG } from "./ipc"; 3 3 import { archive, preload } from "./preload"; 4 - import { Main_main } from './newt'; 4 + import { LSP_checkFile, LSP_codeActionInfo, LSP_compileJS, LSP_hoverInfo, LSP_updateFile } from './newt'; 5 5 6 6 const LOG = console.log 7 7 8 - console.log = (m) => { 9 - LOG(m) 10 - shim.stdout += "\n" + m; 11 - }; 8 + // console.log = (m) => { 9 + // LOG(m) 10 + // shim.stdout += "\n" + m; 11 + // }; 12 + 13 + const invoke = <T extends (...args: any[]) => any>(fun:T, args: Parameters<T>): ReturnType<T> => { 14 + return fun.apply(undefined, args) 15 + } 16 + 17 + const api: API = { 18 + // none of these are promises... 19 + updateFile: LSP_updateFile, 20 + typeCheck(filename) { 21 + shim.stdout = "" 22 + let diags = LSP_checkFile(filename); 23 + let output = shim.stdout 24 + return {diags,output} 25 + }, 26 + hoverInfo: LSP_hoverInfo, 27 + codeActionInfo: LSP_codeActionInfo, 28 + compile: LSP_compileJS, 29 + } 12 30 13 31 const handleMessage = async function <K extends keyof API>(ev: { data: Message<K> }) { 14 32 LOG("HANDLE", ev.data); 15 33 await preload; 16 34 shim.archive = archive; 17 - let key = ev.data.key 18 - if (key === 'typeCheck' || key === 'compile') { 19 - let {id, args: [fileName]} = ev.data 20 - LOG(key, fileName) 21 - const outfile = "out.js"; 22 - const isCompile = key === 'compile'; 23 - if (isCompile) 24 - shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"]; 25 - else 26 - shim.process.argv = ["browser", "newt", fileName, "--top"]; 27 - shim.stdout = ""; 28 - shim.files[outfile] = new TextEncoder().encode("No JS output"); 29 - 30 - try { 31 - Main_main(); 32 - } catch (e) { 33 - // make it clickable in console 34 - console.error(e); 35 - // make it visable on page 36 - shim.stdout += "\n" + String(e); 37 - } 38 - let result = isCompile ? new TextDecoder().decode(shim.files[outfile]) : shim.stdout 39 - sendResponse({id, result}) 40 - } else if (key === 'save') { 41 - let {id, args: [fileName, content]} = ev.data 42 - LOG(`SAVE ${content?.length} to ${fileName}`) 43 - shim.files[fileName] = new TextEncoder().encode(content) 44 - LOG('send', {id, result: ''}) 45 - sendResponse({id, result: ''}) 35 + try { 36 + shim.stdout = '' 37 + let {id, key, args} = ev.data 38 + let result = await invoke(api[key], args) 39 + LOG('got', result) 40 + sendResponse<typeof key>({id, result}) 41 + } catch (e) { 42 + console.error(e) 46 43 } 44 + console.log(shim.stdout) 47 45 48 46 }; 49 47 50 48 // hooks for worker.html to override 51 - let sendResponse: <K extends keyof API>(_: ResponseMSG) => void = postMessage; 49 + let sendResponse: <K extends keyof API>(_: ResponseMSG<K>) => void = postMessage; 52 50 onmessage = handleMessage;
+9 -2
src/Commands.newt
··· 29 29 then go (x :: acc) xs 30 30 else (joinBy "/" (xs :< x <>> Nil), joinBy "." acc) 31 31 32 - 33 32 switchModule : FileSource → String → M (Maybe ModContext) 34 33 switchModule repo modns = do 35 34 -- TODO processing on hover is expensive, but info is not always there ··· 69 68 pure $ HasHover e.fc ("\{show e.name} : \{rpprint Nil ty}") 70 69 71 70 where 71 + -- We don't want to pick up the paren token when on the border 72 + isIdent : BTok → Bool 73 + isIdent (MkBounded (Tok Ident _) _) = True 74 + isIdent (MkBounded (Tok UIdent _) _) = True 75 + isIdent (MkBounded (Tok MixFix _) _) = True 76 + isIdent (MkBounded (Tok Projection _) _) = True 77 + isIdent _ = False 78 + 72 79 getTok : List BTok → Maybe String 73 80 getTok Nil = Nothing 74 81 getTok (tok :: toks) = 75 - if tok.bounds.startCol <= col && (col <= tok.bounds.endCol) 82 + if tok.bounds.startCol <= col && col <= tok.bounds.endCol && isIdent tok 76 83 then Just $ value tok else getTok toks 77 84 78 85 data FileEdit = MkEdit FC String
+23 -1
src/LSP.newt
··· 16 16 import Lib.ProcessDecl 17 17 import Lib.Prettier 18 18 import Lib.Error 19 + import Lib.Compile 19 20 20 21 pfunc js_castArray : Array JSObject → JSObject := `x => x` 21 22 pfunc js_castInt : Int → JSObject := `x => x` ··· 224 225 modifyIORef state $ [ topContext := top ] 225 226 pure $ jsonToJObject $ JsonArray json 226 227 227 - #export updateFile checkFile hoverInfo codeActionInfo 228 + compileJS : String → JSObject 229 + compileJS fn = unsafePerformIO $ do 230 + let (base, modName) = decomposeName fn 231 + st <- readIORef state 232 + when (st.baseDir /= base) $ \ _ => resetState base 233 + repo <- lspFileSource 234 + (Right (top, src)) <- (do 235 + putStrLn "woo" 236 + mod <- processModule emptyFC repo Nil modName 237 + docs <- compile 238 + let src = unlines $ 239 + ( "const bouncer = (f,ini) => { let obj = ini; while (obj.tag) obj = f(obj); return obj.h0 };" 240 + :: Nil) 241 + ++ map (render 90 ∘ noAlt) docs 242 + pure src).runM st.topContext 243 + | Left err => pure $ js_castStr "// \{errorMsg err}" 244 + modifyIORef state [ topContext := top ] 245 + pure $ js_castStr src 246 + 247 + 248 + 249 + #export updateFile checkFile hoverInfo codeActionInfo compileJS
+8 -8
src/Lib/Derive.newt
··· 37 37 38 38 where 39 39 arr : Raw → Raw → Raw 40 - arr a b = RPi emptyFC (BI fc "_" Explicit Many) a b 40 + arr a b = RPi fc (BI fc "_" Explicit Many) a b 41 41 42 42 rvar : String → Raw 43 - rvar nm = RVar emptyFC nm 43 + rvar nm = RVar fc nm 44 44 45 45 getExplictNames : SnocList String → Tm → List String 46 46 getExplictNames acc (Pi fc nm Explicit quant a b) = getExplictNames (acc :< nm) b ··· 49 49 getExplictNames acc _ = acc <>> Nil 50 50 51 51 buildApp : String → List Raw → Raw 52 - buildApp nm nms = foldl (\ t u => RApp emptyFC t u Explicit) (rvar nm) $ nms 52 + buildApp nm nms = foldl (\ t u => RApp fc t u Explicit) (rvar nm) $ nms 53 53 54 54 equate : (Raw × Raw) → Raw 55 55 equate (a,b) = buildApp "_==_" (a :: b :: Nil) ··· 89 89 90 90 where 91 91 arr : Raw → Raw → Raw 92 - arr a b = RPi emptyFC (BI fc "_" Explicit Many) a b 92 + arr a b = RPi fc (BI fc "_" Explicit Many) a b 93 93 94 94 rvar : String → Raw 95 - rvar nm = RVar emptyFC nm 95 + rvar nm = RVar fc nm 96 96 97 97 lstring : String → Raw 98 - lstring s = RLit emptyFC (LString s) 98 + lstring s = RLit fc (LString s) 99 99 100 100 getExplictNames : SnocList String → Tm → List String 101 101 getExplictNames acc (Pi fc nm Explicit quant a b) = getExplictNames (acc :< nm) b ··· 104 104 getExplictNames acc _ = acc <>> Nil 105 105 106 106 buildApp : String → List Raw → Raw 107 - buildApp nm nms = foldl (\ t u => RApp emptyFC t u Explicit) (rvar nm) $ nms 107 + buildApp nm nms = foldl (\ t u => RApp fc t u Explicit) (rvar nm) $ nms 108 108 109 109 equate : (Raw × Raw) → Raw 110 110 equate (a,b) = buildApp "_==_" (a :: b :: Nil) ··· 118 118 let names = getExplictNames Lin ty 119 119 anames <- map rvar <$> traverse freshName names 120 120 let left = buildApp "show" $ buildApp nm anames :: Nil 121 - let shows = map (\ nm => RApp emptyFC (rvar "show") nm Explicit) anames 121 + let shows = map (\ nm => RApp fc (rvar "show") nm Explicit) anames 122 122 let right = case anames of 123 123 Nil => lstring nm 124 124 _ =>