From 3b84ad4a835bb565c87712cbad9d5575068b70fa Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Wed, 4 Oct 2023 14:10:46 -0500 Subject: [PATCH] initial --- .devcontainer/devcontainer.json | 26 +++++ .gitignore | 3 + .prettierrc.json | 7 ++ Dockerfile | 4 + Makefile | 2 + bootstrap/gc.boot | 0 bootstrap/runtime.boot | 6 ++ bun.lockb | Bin 0 -> 6246 bytes package.json | 7 ++ src/index.ts | 25 +++++ src/parser.ts | 170 ++++++++++++++++++++++++++++++++ src/util.ts | 125 +++++++++++++++++++++++ tsconfig.json | 21 ++++ 13 files changed, 396 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 .prettierrc.json create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 bootstrap/gc.boot create mode 100644 bootstrap/runtime.boot create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/parser.ts create mode 100644 src/util.ts create mode 100644 tsconfig.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..057c3dc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile +{ + "name": "Existing Dockerfile", + "build": { + // Sets the run context to one level up instead of the .devcontainer folder. + "context": "..", + // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. + "dockerfile": "../Dockerfile" + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": {} + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a54cc37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/build +node_modules \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bbeb3e0 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "useTabs": false, + "tabWidth": 2, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c7497eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM oven/bun:alpine + +RUN apk add \ + git clang make lld \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a02a791 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +watch: + bun --watch src/index.ts \ No newline at end of file diff --git a/bootstrap/gc.boot b/bootstrap/gc.boot new file mode 100644 index 0000000..e69de29 diff --git a/bootstrap/runtime.boot b/bootstrap/runtime.boot new file mode 100644 index 0000000..5de5568 --- /dev/null +++ b/bootstrap/runtime.boot @@ -0,0 +1,6 @@ +extern main; +extern helloge; + +pub fn _start() { + main(); +} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..3760b10215bab52f586f3bbb6e18519b27833b30 GIT binary patch literal 6246 zcmeHL2~<;O77iFJqUhj`TO*3C%FE88hQ$TZjt4|&r3%py9wAB=^Abi-G>$t~#e!C$ zg5WlyxPpQkR$KrF0kN?P7PK-|K`Dw`MeO|_`62WuP|ut>GiQ3wdAaZZ-~GOO|Lwk) zJ8URNM`<|0awUgUMzDf)N-MaCaFuj^s63n`LRA`tL`y_)t;`t=M)~Eu*vK(^z3X53 zO!kahRLAXZlkR=PQDwF>ezkgH@mfPaFar5YlVVu@PV0^B>Da*7W-z3R5JF2a7$o#q zF&O&&Mm^i8L&#vA4DJ`#0Q|ptfuzk6hJLBl+?DdOkWQA*bR@*_iZOUfv>r7Ld-?!1KAQ=Idqj5Yi z?hXsas{kPad}kxl)Mr~{7@ujxTYMnC&WJ~T5hvy~(!Ez0(kIy{Ps0}%h);&NY zd7nnluy3UQ^ZpxqG@^k2&0@xA;x(maCgs;HJz&i$$~R+|zWw;wh|mj1dG+(Jhzq0A zcBj^lJCroj=T^6=C6_Jh_B`tIPiNl+J2KbVe66tc&wiWdmptVz#E5zEx}&g}(xrV| zhnA;}pKrBEHnHsS)X21x)vr>hm|e2C2PyjxMLF9Rd2IgTx4d(yE?FaLWcyr%n(nRa)&*#Fw};O7oYXZS{mW_Im=w_re0|4;89n~`{xdHrx; zuPe^urj3jkPUU}0z1cFA@7FhH$wQZaz1e+@Dp)IkbCjBD<65p)qZOjXP-ahL_qzCxPxQAqeKlgQ68^uZd<(1>msYp z?wG|xyE=od@6}~XPYry1UigD}b+9E1N0F01zQP6toB)!M{9ZY);* zIwiwI_m z(A<*i^{nV&85z5kS{{3!bw1TnT3bL8gWQ`&z>f{@a@$sOivqgx4hRewRNflE6Ej$!K388 zZ`N#Vj2PozGV-jzJ>io%7M>N3q1(nv0uUXnV;obM&)miHZot-V8-<)h78@@#1XqBak8kjsYM5KGxr3DxqghG=OSs!cN0wa8Opy{n4HFu(*-=W z;j~vBu`);$`lC}oudLOxZl%xutoTsua7DixSFf@+R8?0_xRM84S%Z`W^RuIe1|TDgMH3ctU&n_=Z^oUJ~Zd} zzrsvN%J7FpkEZEeeW<^qV){PnDCSSQKWE^7Wd^ny|Jk22YAX??k+S7VN-GHuXXCO0 z+h0SHZY(~RMaiQ`Rj30?$dZK1WlB}3$&0D+ZIhs}Sv)I2U;>j}g43|v>Rq!SJF>T$-x zl?O{Rv4G8G3tDO@xHe&q0;2_2V{lc(+)T`6^8nUdcWK6$iG{$%WiW7s23JY|BV-HM zLIwlZaBwXJp77XwHvC;9!5hqg9#>u9iBl8Xe&Z8dhk+-K@PyY=0m79Sb2C00=)fM= zesB$jj(~?^!&M?&t$`=b0D>pDZiMSMG=?4zt{~w`4m{yEMSyEcxVB?%wuA?4Fw(8Y z6~nsq?x#pa1gT+rF4U45r6k->tC1^ZZY*BQqPjFMYMznlsIR) zLP9E0N;HvdK9?`xak)Z&o{-BE@K8lh8>uF#Fs)WijpcAOq)bj}!BDAI%%j*Ujf~T@ z3-UD%E-U4;a5S_31W?cnJ3Xo?r0CM|lx zXfz!@3_=MzUfaenTG^qQLBqy4m{a4PY3f!=6lA+x$f%hJHDKN0aJDm~)+^%(iHjZv ws*B?U%9hS+>i}Uh@QrT?I?*D}?>?7pjHUU=0$uYxjO6`?9J!&B_kZEfZ;0&f*8l(j literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..8847304 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { "chalk": "^5.3.0", "cmd-ts": "^0.13.0" }, + "devDependencies": { + "bun-types": "^1.0.4-canary.20231004T140131", + "prettier": "^3.0.3" + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..da63fcf --- /dev/null +++ b/src/index.ts @@ -0,0 +1,25 @@ +import { BunFile } from "bun"; +import { Type, command, run, positional, boolean, option, flag } from "cmd-ts"; +import { parseProgram, program } from "./parser"; + +const File: Type = { + from: async (str) => Bun.file(str), +}; + +const app = command({ + name: "", + args: { + lib: flag({ long: "lib", description: "Compile this in library mode (no main function)" }), + file: positional({ type: File, displayName: "file", description: "Source code" }), + }, + handler: async ({ lib, file }) => { + // Read stream to file + const source = await file.text(); + + // Parse source + const parseResult = parseProgram(source); + // console.log("tree", parseResult); + }, +}); + +run(app, Bun.argv.slice(2)); diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..901267f --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,170 @@ +import { Result, getLinesAround } from "./util"; +import chalk from "chalk"; + +export interface ParseInput { + source: string; + position: number; +} + +export interface ParseSuccess { + output: T; + start: number; + end: number; +} + +export interface ParseError { + expected?: string; + start: number; +} + +export interface Parser { + (_: ParseInput): Result, ParseError>; +} + +export const exact: (_: string) => Parser = (needle: string) => (input: ParseInput) => { + const start = input.position; + const end = start + needle.length; + const sliced = input.source.slice(start, end); + return sliced === needle + ? Result.ok({ output: sliced, start, end }) + : Result.err({ expected: `The string ${JSON.stringify(needle)}`, start }); +}; + +export const regex: (_: RegExp) => Parser = (re: RegExp) => (input: ParseInput) => { + const start = input.position; + const reY = new RegExp(re, [...new Set([...re.flags.split(""), "y", "m"])].join("")); + + reY.lastIndex = start; + const result = input.source.match(reY); + if (!result) return Result.err({ expected: `String that matches ${reY}`, start }); + return Result.ok({ output: result[0], start, end: start + result[0].length }); +}; + +export const optWhitespace = regex(/\s*/m); +export const whitespace = regex(/\s+/m); + +export const surroundWs = (parser: Parser) => + first(second(optWhitespace, parser), optWhitespace); + +export const map = + (parser: Parser, func: (_: T) => U) => + (input: ParseInput) => + parser(input).map(({ output, ...result }) => ({ output: func(output), ...result })); + +export const pair: (p1: Parser, p2: Parser) => Parser<[Out1, Out2]> = + (p1, p2) => (input: ParseInput) => + p1(input).andThen(({ output: out1, end }) => + p2({ ...input, position: end }).map(({ output: out2, end }) => ({ + output: [out1, out2], + start: input.position, + end: end, + })), + ); + +export const first: (p1: Parser, p2: Parser) => Parser = (p1, p2) => + map(pair(p1, p2), ([out1]) => out1); + +export const firstWs: (p1: Parser, p2: Parser) => Parser = (p1, p2) => + first(first(p1, whitespace), p2); + +export const second: (p1: Parser, p2: Parser) => Parser = (p1, p2) => + map(pair(p1, p2), ([_, out2]) => out2); + +export const secondWs: (p1: Parser, p2: Parser) => Parser = ( + p1, + p2, +) => second(second(p1, whitespace), p2); + +export const sep: (itemParser: Parser, delimParser: Parser) => Parser = + (itemParser, delimParser) => (input: ParseInput) => { + const items = []; + let position = input.position; + let first = true; + while (true) { + if (first) { + first = false; + } else { + const result = delimParser({ ...input, position }); + if (result.status === "err") break; + const { end } = result.unwrap(); + position = end; + } + + const result = itemParser({ ...input, position }); + if (result.status === "err") break; + const { output, end } = result.unwrap(); + position = end; + items.push(output); + } + return Result.ok({ output: items, start: input.position, end: position }); + }; + +export const repeatWs: (parser: Parser) => Parser = (parser) => sep(parser, whitespace); + +export const alt = + (...parsers: Parser[]) => + (input: ParseInput) => + parsers.reduceRight( + (prevValue: Result, ParseError>, nextValue: Parser) => + prevValue.orElse(() => nextValue(input)), + Result.err({ start: input.position }), + ); + +// Language-specific stuff + +export const ident = regex(/[_a-zA-Z][_a-zA-Z0-9]+/); + +export const extern = map(first(secondWs(exact("extern"), ident), exact(";")), (name) => ({ + kind: "externItem", + name, +})); + +export const item = alt(extern); + +export const program = surroundWs(repeatWs(item)); + +export function parseProgram(input: string) { + const result = program({ source: input, position: 0 }); + if (result.status === "err") { + const { expected, start } = result.unwrapErr(); + const expectedMsg = expected ? `Expected: ${expected}` : ""; + printError(input, start, expectedMsg); + } + + const { output, end } = result.unwrap(); + if (end !== input.length) { + printError(input, end, "wtf is this bro"); + } + + return output; +} + +function printError(input: string, at: number, msg: string) { + const linesAround = getLinesAround(input, at); + if (linesAround.status === "err") throw new Error(msg); + + const { lines, focusedLineIdx, startLineNum, col } = linesAround.unwrap(); + + const maxLen = Math.max( + startLineNum.toString().length, + (startLineNum + lines.length).toString().length, + ); + + console.log(chalk.red("Error parsing program:")); + + lines.forEach((line, idx) => { + const absLine = idx + startLineNum; + const absLineStr = absLine.toString().padStart(maxLen, " "); + + console.log(chalk.blue(`${absLineStr} | `) + line); + if (idx === focusedLineIdx) { + const emptyLineStr = "".padStart(maxLen, " "); + const errorPointer = new Array(col).fill(0).map((_) => " ") + "^"; + console.log(chalk.blue(`${emptyLineStr} | `) + chalk.red(errorPointer + " " + msg)); + } + }); + + console.log(); + + throw new Error(); +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..b729c37 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,125 @@ +export class Result { + private constructor( + public status: "ok" | "err", + private okValue: T | undefined, + private errValue: E | undefined, + ) {} + + toString(): string { + switch (this.status) { + case "ok": + return `Ok(${this.okValue!.toString()})`; + case "err": + return `Err(${this.errValue!.toString()})`; + } + } + + static ok(value: T): Result { + return new Result("ok", value, undefined!); + } + + static err(value: E): Result { + return new Result("err", undefined!, value); + } + + unwrap(): T { + switch (this.status) { + case "ok": + return this.okValue!; + case "err": + throw new Error("unwrap"); + } + } + + unwrapErr(): E { + switch (this.status) { + case "ok": + throw new Error("unwrap"); + case "err": + return this.errValue!; + } + } + + map(func: (_: T) => U): Result { + switch (this.status) { + case "ok": + return Result.ok(func(this.okValue!)); + case "err": + return Result.err(this.errValue!); + } + } + + mapErr(func: (_: E) => U): Result { + switch (this.status) { + case "ok": + return Result.ok(this.okValue!); + case "err": + return Result.err(func(this.errValue!)); + } + } + + andThen(func: (_: T) => Result): Result { + switch (this.status) { + case "ok": + return func(this.okValue!); + case "err": + return Result.err(this.errValue!); + } + } + + orElse(func: (_: E) => Result): Result { + switch (this.status) { + case "ok": + return Result.ok(this.okValue!); + case "err": + return func(this.errValue!); + } + } +} + +export interface LinesAround { + lines: string[]; + startLineNum: number; + focusedLineIdx: number; + col: number; +} + +export function getLinesAround(source: string, at: number): Result { + let pos = 0; + let lineIdx = 0; + let focusedLineIdx; + let col; + let allLines = []; + + while (true) { + const re = /\r?\n/; + const nextNewline = source.slice(pos).match(re); + + if (!nextNewline) break; + const line = source.slice(pos, pos + nextNewline.index); + allLines.push(line); + + const start = pos; + const end = start + line.length; + + if (at >= start && at < end) { + focusedLineIdx = lineIdx; + col = at - start; + } + + pos += line.length + nextNewline.length; + lineIdx += 1; + } + + if (focusedLineIdx === undefined || col === undefined) return Result.err(undefined); + + const before = Math.max(0, focusedLineIdx - 1); + const after = Math.min(allLines.length - 1, focusedLineIdx + 1); + + return Result.ok({ + lines: allLines.slice(before, after + 1), + focusedLineIdx: focusedLineIdx - before, + startLineNum: before + 1, + col, + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b559b72 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + + "moduleResolution": "bundler", + "noEmit": true, + "allowImportingTsExtensions": true, + "moduleDetection": "force", + + "strict": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "composite": true, + "downlevelIteration": true, + "allowSyntheticDefaultImports": true, + + "types": ["bun-types"] + } +}