commit 3b84ad4a835bb565c87712cbad9d5575068b70fa Author: Michael Zhang Date: Wed Oct 4 14:10:46 2023 -0500 initial 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 0000000..3760b10 Binary files /dev/null and b/bun.lockb differ 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"] + } +}