initial
This commit is contained in:
commit
3b84ad4a83
13 changed files with 396 additions and 0 deletions
26
.devcontainer/devcontainer.json
Normal file
26
.devcontainer/devcontainer.json
Normal file
|
@ -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"
|
||||
}
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
/build
|
||||
node_modules
|
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
4
Dockerfile
Normal file
4
Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
|||
FROM oven/bun:alpine
|
||||
|
||||
RUN apk add \
|
||||
git clang make lld
|
2
Makefile
Normal file
2
Makefile
Normal file
|
@ -0,0 +1,2 @@
|
|||
watch:
|
||||
bun --watch src/index.ts
|
0
bootstrap/gc.boot
Normal file
0
bootstrap/gc.boot
Normal file
6
bootstrap/runtime.boot
Normal file
6
bootstrap/runtime.boot
Normal file
|
@ -0,0 +1,6 @@
|
|||
extern main;
|
||||
extern helloge;
|
||||
|
||||
pub fn _start() {
|
||||
main();
|
||||
}
|
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
7
package.json
Normal file
7
package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
25
src/index.ts
Normal file
25
src/index.ts
Normal file
|
@ -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<string, BunFile> = {
|
||||
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));
|
170
src/parser.ts
Normal file
170
src/parser.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
import { Result, getLinesAround } from "./util";
|
||||
import chalk from "chalk";
|
||||
|
||||
export interface ParseInput {
|
||||
source: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export interface ParseSuccess<T> {
|
||||
output: T;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface ParseError {
|
||||
expected?: string;
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface Parser<Out> {
|
||||
(_: ParseInput): Result<ParseSuccess<Out>, ParseError>;
|
||||
}
|
||||
|
||||
export const exact: (_: string) => Parser<string> = (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<string> = (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 = <T>(parser: Parser<T>) =>
|
||||
first(second(optWhitespace, parser), optWhitespace);
|
||||
|
||||
export const map =
|
||||
<T, U>(parser: Parser<T>, func: (_: T) => U) =>
|
||||
(input: ParseInput) =>
|
||||
parser(input).map(({ output, ...result }) => ({ output: func(output), ...result }));
|
||||
|
||||
export const pair: <Out1, Out2>(p1: Parser<Out1>, p2: Parser<Out2>) => 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: <Out1, Out2>(p1: Parser<Out1>, p2: Parser<Out2>) => Parser<Out1> = (p1, p2) =>
|
||||
map(pair(p1, p2), ([out1]) => out1);
|
||||
|
||||
export const firstWs: <Out1, Out2>(p1: Parser<Out1>, p2: Parser<Out2>) => Parser<Out1> = (p1, p2) =>
|
||||
first(first(p1, whitespace), p2);
|
||||
|
||||
export const second: <Out1, Out2>(p1: Parser<Out1>, p2: Parser<Out2>) => Parser<Out2> = (p1, p2) =>
|
||||
map(pair(p1, p2), ([_, out2]) => out2);
|
||||
|
||||
export const secondWs: <Out1, Out2>(p1: Parser<Out1>, p2: Parser<Out2>) => Parser<Out2> = (
|
||||
p1,
|
||||
p2,
|
||||
) => second(second(p1, whitespace), p2);
|
||||
|
||||
export const sep: <T, D>(itemParser: Parser<T>, delimParser: Parser<D>) => Parser<T[]> =
|
||||
(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: <T>(parser: Parser<T>) => Parser<T[]> = (parser) => sep(parser, whitespace);
|
||||
|
||||
export const alt =
|
||||
<T>(...parsers: Parser<T>[]) =>
|
||||
(input: ParseInput) =>
|
||||
parsers.reduceRight(
|
||||
(prevValue: Result<ParseSuccess<T>, ParseError>, nextValue: Parser<T>) =>
|
||||
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();
|
||||
}
|
125
src/util.ts
Normal file
125
src/util.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
export class Result<T, E> {
|
||||
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<T, E>(value: T): Result<T, E> {
|
||||
return new Result("ok", value, undefined!);
|
||||
}
|
||||
|
||||
static err<T, E>(value: E): Result<T, E> {
|
||||
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<U>(func: (_: T) => U): Result<U, E> {
|
||||
switch (this.status) {
|
||||
case "ok":
|
||||
return Result.ok(func(this.okValue!));
|
||||
case "err":
|
||||
return Result.err(this.errValue!);
|
||||
}
|
||||
}
|
||||
|
||||
mapErr<U>(func: (_: E) => U): Result<T, U> {
|
||||
switch (this.status) {
|
||||
case "ok":
|
||||
return Result.ok(this.okValue!);
|
||||
case "err":
|
||||
return Result.err(func(this.errValue!));
|
||||
}
|
||||
}
|
||||
|
||||
andThen<U>(func: (_: T) => Result<U, E>): Result<U, E> {
|
||||
switch (this.status) {
|
||||
case "ok":
|
||||
return func(this.okValue!);
|
||||
case "err":
|
||||
return Result.err(this.errValue!);
|
||||
}
|
||||
}
|
||||
|
||||
orElse<U>(func: (_: E) => Result<U, E>): Result<U, E> {
|
||||
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<LinesAround, undefined> {
|
||||
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,
|
||||
});
|
||||
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
@ -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"]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue