Add WIP
This commit is contained in:
parent
75e5928f9e
commit
f1d35917f2
15 changed files with 495 additions and 204 deletions
18
README.md
18
README.md
|
@ -27,12 +27,6 @@ print(fac(5)) // 10
|
|||
|
||||
## FAQ
|
||||
|
||||
### What made you write Bolt?
|
||||
|
||||
Because I was tired of writing applications in the horror that JavaScript
|
||||
is. Moreover, I usually write these applications by myself, so I have to be
|
||||
clever about how they will be implemented.
|
||||
|
||||
### Why should I choose Bolt over JavaScript?
|
||||
|
||||
Bolt was made to make writing user-interfaces dead-simple, while also making
|
||||
|
@ -48,6 +42,18 @@ about _correctness_, _performance_ and _scalability_.
|
|||
- Scalability, because just like Rust, Bolt takes a _functional_ approach to
|
||||
software design using type traits, favouring composition over inheritance.
|
||||
|
||||
### What languages inspired Bolt?
|
||||
|
||||
Rust and Haskell are two of my favorite languages that you'll notice Bolt
|
||||
shares a lot of its syntax and semantics with.
|
||||
|
||||
### What's the difference between Bolt and Rust?
|
||||
|
||||
I really like Rust, but if I just care about writing an application I believe
|
||||
Rust's memory model with its borrow checker is overkill. Having a garbage
|
||||
collector certainly results in a performance penalty, but I believe that as
|
||||
long as the user does not notice it, it does not really matter.
|
||||
|
||||
## License
|
||||
|
||||
Bolt itself is licensed under the GPL-3.0, because we put a lot of work in it
|
||||
|
|
59
src/ast.ts
59
src/ast.ts
|
@ -1,6 +1,8 @@
|
|||
|
||||
// FIXME SyntaxBase.getSpan() does not work then [n1, n2] is given as origNode
|
||||
|
||||
import * as path from "path"
|
||||
|
||||
import { Stream, StreamWrapper } from "./util"
|
||||
import { Scanner } from "./scanner"
|
||||
import { RecordType, PrimType, OptionType, VariantType, stringType, intType, boolType } from "./checker"
|
||||
|
@ -41,6 +43,7 @@ export enum SyntaxKind {
|
|||
ModKeyword,
|
||||
EnumKeyword,
|
||||
StructKeyword,
|
||||
NewTypeKeyword,
|
||||
|
||||
// Special nodes
|
||||
|
||||
|
@ -86,15 +89,20 @@ export enum SyntaxKind {
|
|||
ImportDecl,
|
||||
RecordDecl,
|
||||
VariantDecl,
|
||||
NewTypeDecl,
|
||||
|
||||
}
|
||||
|
||||
export class TextFile {
|
||||
|
||||
constructor(public path: string) {
|
||||
constructor(public path: string, public cwd: string = '.') {
|
||||
|
||||
}
|
||||
|
||||
get fullPath() {
|
||||
return path.resolve(this.cwd, this.path)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class TextPos {
|
||||
|
@ -588,7 +596,7 @@ export class QualName {
|
|||
for (const chunk of this.path) {
|
||||
out += chunk.text + '.'
|
||||
}
|
||||
return out + this.name
|
||||
return out + this.name.text
|
||||
}
|
||||
|
||||
toJSON(): Json {
|
||||
|
@ -982,7 +990,7 @@ export class FuncDecl extends SyntaxBase {
|
|||
public name: QualName,
|
||||
public params: Param[],
|
||||
public returnType: TypeDecl | null,
|
||||
public body: Stmt[] | null,
|
||||
public body: Body | null,
|
||||
public span: TextSpan | null = null,
|
||||
public origNode: [Syntax, Syntax] | Syntax | null = null,
|
||||
public parentNode: Syntax | null = null
|
||||
|
@ -990,18 +998,18 @@ export class FuncDecl extends SyntaxBase {
|
|||
super();
|
||||
}
|
||||
|
||||
toJSON(): Json {
|
||||
return {
|
||||
kind: 'FuncDecl',
|
||||
isPublic: this.isPublic,
|
||||
target: this.target,
|
||||
name: this.name.toJSON(),
|
||||
params: this.params.map(p => p.toJSON()),
|
||||
returnType: this.returnType !== null ? this.returnType.toJSON() : null,
|
||||
body: this.body !== null ? this.body.map(s => s.toJSON()) : null,
|
||||
span: this.span !== null ? this.span.toJSON() : this.span,
|
||||
}
|
||||
}
|
||||
// toJSON(): Json {
|
||||
// return {
|
||||
// kind: 'FuncDecl',
|
||||
// isPublic: this.isPublic,
|
||||
// target: this.target,
|
||||
// name: this.name.toJSON(),
|
||||
// params: this.params.map(p => p.toJSON()),
|
||||
// returnType: this.returnType !== null ? this.returnType.toJSON() : null,
|
||||
// body: this.body !== null ? this.body.map(s => s.toJSON()) : null,
|
||||
// span: this.span !== null ? this.span.toJSON() : this.span,
|
||||
// }
|
||||
// }
|
||||
|
||||
*getChildren(): IterableIterator<Syntax> {
|
||||
yield this.name
|
||||
|
@ -1105,10 +1113,31 @@ export class RecordDecl extends SyntaxBase {
|
|||
|
||||
}
|
||||
|
||||
export class NewTypeDecl extends SyntaxBase {
|
||||
|
||||
kind: SyntaxKind.NewTypeDecl = SyntaxKind.NewTypeDecl;
|
||||
|
||||
constructor(
|
||||
public isPublic: boolean,
|
||||
public name: Identifier,
|
||||
public span: TextSpan | null = null,
|
||||
public origNode: [Syntax, Syntax] | Syntax | null = null,
|
||||
public parentNode: Syntax | null = null
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
*getChildren() {
|
||||
yield this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type Decl
|
||||
= Sentence
|
||||
| FuncDecl
|
||||
| ImportDecl
|
||||
| NewTypeDecl
|
||||
| VarDecl
|
||||
| RecordDecl
|
||||
|
||||
|
|
213
src/bin/bolt.ts
213
src/bin/bolt.ts
|
@ -9,14 +9,13 @@ import { spawnSync } from "child_process"
|
|||
|
||||
import yargs from "yargs"
|
||||
|
||||
import { Scanner } from "../scanner"
|
||||
import { Parser } from "../parser"
|
||||
import { Expander } from "../expander"
|
||||
import { TypeChecker } from "../checker"
|
||||
import { Compiler } from "../compiler"
|
||||
import { Evaluator } from "../evaluator"
|
||||
import { Emitter } from "../emitter"
|
||||
import { TextFile, SourceFile, setParents } from "../ast"
|
||||
import { Package, loadPackage } from "../package"
|
||||
import { Program } from "../program"
|
||||
import { TextFile } from "../ast"
|
||||
|
||||
global.debug = function (value: any) {
|
||||
console.error(require('util').inspect(value, { depth: Infinity, colors: true }))
|
||||
}
|
||||
|
||||
function toArray<T>(value: T | T[]): T[] {
|
||||
if (Array.isArray(value)) {
|
||||
|
@ -44,178 +43,68 @@ function flatMap<T>(array: T[], proc: (element: T) => T[]) {
|
|||
return out
|
||||
}
|
||||
|
||||
interface Hook {
|
||||
timing: 'before' | 'after'
|
||||
name: string
|
||||
effects: string[]
|
||||
}
|
||||
|
||||
function parseHook(str: string): Hook {
|
||||
let timing: 'before' | 'after' = 'before';
|
||||
if (str[0] === '+') {
|
||||
str = str.substring(1)
|
||||
timing = 'after';
|
||||
}
|
||||
const [name, rawEffects] = str.split('=');
|
||||
return {
|
||||
timing,
|
||||
name,
|
||||
effects: rawEffects.split(','),
|
||||
}
|
||||
}
|
||||
|
||||
yargs
|
||||
|
||||
.command(
|
||||
|
||||
'compile [files..]',
|
||||
'Compile a set of source files',
|
||||
|
||||
yargs => yargs
|
||||
.string('hook')
|
||||
.describe('hook', 'Add a hook to a specific compile phase. See the manual for details.'),
|
||||
'link [name]',
|
||||
'Link projects with each other',
|
||||
|
||||
yargs => yargs,
|
||||
|
||||
args => {
|
||||
|
||||
const hooks: Hook[] = toArray(args.hook as string[] | string).map(parseHook);
|
||||
const sourceFiles: SourceFile[] = [];
|
||||
console.log(args.name)
|
||||
|
||||
for (const filepath of toArray(args.files as string[] | string)) {
|
||||
|
||||
const file = new TextFile(filepath);
|
||||
const content = fs.readFileSync(filepath, 'utf8')
|
||||
const scanner = new Scanner(file, content)
|
||||
|
||||
for (const hook of hooks) {
|
||||
if (hook.name === 'scan' && hook.timing === 'before') {
|
||||
for (const effect of hook.effects) {
|
||||
switch (effect) {
|
||||
case 'abort':
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Could not execute hook effect '${effect}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceFile = scanner.scan();
|
||||
// while (true) {
|
||||
// const token = scanner.scanToken()
|
||||
// if (token === null) {
|
||||
// break;
|
||||
// }
|
||||
// tokens.push(token);
|
||||
// }
|
||||
|
||||
for (const hook of hooks) {
|
||||
if (hook.name === 'scan' && hook.timing == 'after') {
|
||||
for (const effect of hook.effects) {
|
||||
switch (effect) {
|
||||
case 'dump':
|
||||
console.log(JSON.stringify(sourceFile.toJSON(), undefined, 2));
|
||||
break;
|
||||
case 'abort':
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Could not execute hook effect '${effect}'.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceFiles.push(sourceFile);
|
||||
|
||||
}
|
||||
|
||||
const checker = new TypeChecker()
|
||||
|
||||
for (const sourceFile of sourceFiles) {
|
||||
const parser = new Parser()
|
||||
const evaluator = new Evaluator(checker)
|
||||
const expander = new Expander(parser, evaluator, checker)
|
||||
const expandedSourceFile = expander.getFullyExpanded(sourceFile)
|
||||
|
||||
for (const hook of hooks) {
|
||||
if (hook.name === 'expand' && hook.timing == 'after') {
|
||||
for (const effect of hook.effects) {
|
||||
switch (effect) {
|
||||
case 'dump':
|
||||
console.log(JSON.stringify(expandedSourceFile.toJSON(), undefined, 2));
|
||||
break;
|
||||
case 'abort':
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Could not execute hook effect '${effect}'.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
.command(
|
||||
|
||||
'exec [files..]',
|
||||
'Run the specified Bolt scripts',
|
||||
|
||||
yargs =>
|
||||
yargs,
|
||||
|
||||
args => {
|
||||
|
||||
const parser = new Parser()
|
||||
const checker = new TypeChecker()
|
||||
|
||||
const sourceFiles = toArray(args.files as string[]).map(filepath => {
|
||||
const file = new TextFile(filepath)
|
||||
const contents = fs.readFileSync(filepath, 'utf8')
|
||||
const scanner = new Scanner(file, contents)
|
||||
const sourceFile = scanner.scan();
|
||||
const evaluator = new Evaluator(checker)
|
||||
const expander = new Expander(parser, evaluator, checker)
|
||||
const expanded = expander.getFullyExpanded(sourceFile) as SourceFile;
|
||||
// console.log(require('util').inspect(expanded.toJSON(), { colors: true, depth: Infinity }))
|
||||
setParents(expanded)
|
||||
return expanded;
|
||||
})
|
||||
|
||||
const compiler = new Compiler(checker, { target: "JS" })
|
||||
const bundle = compiler.compile(sourceFiles)
|
||||
const emitter = new Emitter()
|
||||
const outfiles = bundle.map(file => {
|
||||
const text = emitter.emit(file);
|
||||
fs.mkdirpSync('.bolt-work')
|
||||
const filepath = path.join('.bolt-work', path.relative(process.cwd(), stripExtension(path.resolve(file.loc.source)) + '.mjs'))
|
||||
fs.writeFileSync(filepath, text, 'utf8')
|
||||
return filepath
|
||||
})
|
||||
|
||||
spawnSync('node', [outfiles[0]], { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
.command(
|
||||
|
||||
'dump [file]',
|
||||
'Dump a representation of a given primitive node to disk',
|
||||
'bundle [files..]',
|
||||
'Compile and optimise a set of Bolt packages/scripts',
|
||||
|
||||
yargs => yargs,
|
||||
yargs => yargs
|
||||
.string('work-dir')
|
||||
.describe('work-dir', 'The working directory where files will be resolved against.')
|
||||
.default('work-dir', '.'),
|
||||
|
||||
args => {
|
||||
|
||||
const file = new TextFile(args.file as string)
|
||||
const contents = fs.readFileSync(args.file, 'utf8')
|
||||
const scanner = new Scanner(file, contents)
|
||||
const parser = new Parser();
|
||||
const patt = parser.parsePattern(scanner)
|
||||
console.log(JSON.stringify(patt.toJSON(), undefined, 2))
|
||||
const files = toArray(args.path as string[] | string).map(filepath => new TextFile(filepath, args['work-dir']));
|
||||
const program = new Program(files)
|
||||
program.compile({ target: "JS" });
|
||||
|
||||
})
|
||||
|
||||
.command(
|
||||
|
||||
'exec [files..]',
|
||||
'Run the specified Bolt packages/scripts',
|
||||
|
||||
yargs => yargs
|
||||
.string('work-dir')
|
||||
.describe('work-dir', 'The working directory where files will be resolved against.')
|
||||
.default('work-dir', '.'),
|
||||
|
||||
args => {
|
||||
|
||||
const files = toArray(args.files as string | string[]).map(p => new TextFile(p));
|
||||
|
||||
if (files.length > 0) {
|
||||
|
||||
const program = new Program(files);
|
||||
|
||||
for (const file of files) {
|
||||
program.eval(file)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
throw new Error(`Executing packages is not yet supported.`)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
116
src/checker.ts
116
src/checker.ts
|
@ -16,6 +16,17 @@ export class PrimType extends Type {
|
|||
|
||||
}
|
||||
|
||||
export class FunctionType extends Type {
|
||||
|
||||
constructor(
|
||||
public paramTypes: Type[],
|
||||
public returnType: Type,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class VariantType extends Type {
|
||||
|
||||
constructor(public elementTypes: Type[]) {
|
||||
|
@ -28,6 +39,8 @@ export const stringType = new PrimType()
|
|||
export const intType = new PrimType()
|
||||
export const boolType = new PrimType()
|
||||
export const voidType = new PrimType()
|
||||
export const anyType = new PrimType()
|
||||
export const noneType = new PrimType();
|
||||
|
||||
export class RecordType {
|
||||
|
||||
|
@ -78,6 +91,9 @@ function getFullName(node: Syntax) {
|
|||
let curr: Syntax | null = node;
|
||||
while (true) {
|
||||
switch (curr.kind) {
|
||||
case SyntaxKind.Identifier:
|
||||
out.unshift(curr.text)
|
||||
break;
|
||||
case SyntaxKind.Module:
|
||||
out.unshift(curr.name.fullText);
|
||||
break;
|
||||
|
@ -99,15 +115,56 @@ export class TypeChecker {
|
|||
protected types = new Map<Syntax, Type>();
|
||||
protected scopes = new Map<Syntax, Scope>();
|
||||
|
||||
protected createType(node: Syntax): Type {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
console.log(node)
|
||||
protected inferTypeFromUsage(bindings: Patt, body: Body) {
|
||||
return anyType;
|
||||
}
|
||||
|
||||
protected getTypeOfBody(body: Body) {
|
||||
return anyType;
|
||||
}
|
||||
|
||||
protected createType(node: Syntax): Type {
|
||||
|
||||
switch (node.kind) {
|
||||
|
||||
case SyntaxKind.ConstExpr:
|
||||
return node.value.type;
|
||||
|
||||
case SyntaxKind.NewTypeDecl:
|
||||
console.log(getFullName(node.name))
|
||||
this.symbols[getFullName(node.name)] = new PrimType();
|
||||
return noneType;
|
||||
|
||||
case SyntaxKind.FuncDecl:
|
||||
let returnType = anyType;
|
||||
if (node.returnType !== null) {
|
||||
returnType = this.getTypeOfNode(node.returnType)
|
||||
}
|
||||
if (node.body !== null) {
|
||||
returnType = this.intersectTypes(returnType, this.getTypeOfBody(node.body))
|
||||
}
|
||||
let paramTypes = node.params.map(param => {
|
||||
let paramType = this.getTypeOfNode(param);
|
||||
if (node.body !== null) {
|
||||
paramType = this.intersectTypes(
|
||||
paramType,
|
||||
this.inferTypeFromUsage(param.bindings, node.body)
|
||||
)
|
||||
}
|
||||
return paramType
|
||||
})
|
||||
return new FunctionType(paramTypes, returnType);
|
||||
|
||||
case SyntaxKind.TypeRef:
|
||||
const reffed = this.getTypeNamed(node.name.fullText);
|
||||
if (reffed === null) {
|
||||
throw new Error(`Could not find a type named '${node.name.fullText}'`);
|
||||
}
|
||||
return reffed;
|
||||
|
||||
case SyntaxKind.RecordDecl:
|
||||
|
||||
const typ = new RecordType(map(node.fields, ([name, typ]) => ([name.text, typ])));
|
||||
|
@ -116,17 +173,11 @@ export class TypeChecker {
|
|||
|
||||
return typ;
|
||||
|
||||
// if (typeof node.value === 'bigint') {
|
||||
// return intType;
|
||||
// } else if (typeof node.value === 'string') {
|
||||
// return stringType;
|
||||
// } else if (typeof node.value === 'boolean') {
|
||||
// return boolType;
|
||||
// } else if (isNode(node.value)) {
|
||||
// return this.getTypeNamed(`Bolt.AST.${SyntaxKind[node.value.kind]}`)!
|
||||
// } else {
|
||||
// throw new Error(`Unrecognised kind of value associated with ConstExpr`)
|
||||
// }
|
||||
case SyntaxKind.Param:
|
||||
if (node.typeDecl !== null) {
|
||||
return this.getTypeOfNode(node.typeDecl)
|
||||
}
|
||||
return anyType;
|
||||
|
||||
default:
|
||||
throw new Error(`Could not derive type of ${SyntaxKind[node.kind]}`)
|
||||
|
@ -136,8 +187,8 @@ export class TypeChecker {
|
|||
}
|
||||
|
||||
getTypeNamed(name: string) {
|
||||
return name in this.typeNames
|
||||
? this.typeNames[name]
|
||||
return name in this.symbols
|
||||
? this.symbols[name]
|
||||
: null
|
||||
}
|
||||
|
||||
|
@ -152,13 +203,23 @@ export class TypeChecker {
|
|||
|
||||
check(node: Syntax) {
|
||||
|
||||
this.getTypeOfNode(node);
|
||||
|
||||
switch (node.kind) {
|
||||
|
||||
case SyntaxKind.Sentence:
|
||||
case SyntaxKind.RecordDecl:
|
||||
case SyntaxKind.NewTypeDecl:
|
||||
break;
|
||||
|
||||
case SyntaxKind.RecordDecl:
|
||||
this.getTypeOfNode(node);
|
||||
case SyntaxKind.FuncDecl:
|
||||
if (node.body !== null) {
|
||||
if (Array.isArray(node.body)) {
|
||||
for (const element of node.body) {
|
||||
this.check(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SyntaxKind.Module:
|
||||
|
@ -191,6 +252,27 @@ export class TypeChecker {
|
|||
return scope
|
||||
}
|
||||
|
||||
protected intersectTypes(a: Type, b: Type): Type {
|
||||
if (a === noneType || b == noneType) {
|
||||
return noneType;
|
||||
}
|
||||
if (b === anyType) {
|
||||
return a
|
||||
}
|
||||
if (a === anyType) {
|
||||
return b;
|
||||
}
|
||||
if (a instanceof FunctionType && b instanceof FunctionType) {
|
||||
if (a.paramTypes.length !== b.paramTypes.length) {
|
||||
return noneType;
|
||||
}
|
||||
const returnType = this.intersectTypes(a.returnType, b.returnType);
|
||||
const paramTypes = a.paramTypes.map((_, i) => this.intersectTypes(a.paramTypes[i], b.paramTypes[i]));
|
||||
return new FunctionType(paramTypes, returnType)
|
||||
}
|
||||
return noneType;
|
||||
}
|
||||
|
||||
// getMapperForNode(target: string, node: Syntax): Mapper {
|
||||
// return this.getScope(node).getMapper(target)
|
||||
// }
|
||||
|
|
|
@ -95,6 +95,12 @@ export class Compiler {
|
|||
}
|
||||
|
||||
switch (node.kind) {
|
||||
|
||||
case SyntaxKind.Module:
|
||||
for (const element of node.elements) {
|
||||
this.compileDecl(element, preamble);
|
||||
}
|
||||
break;
|
||||
|
||||
case SyntaxKind.ImportDecl:
|
||||
preamble.push({
|
||||
|
|
|
@ -103,6 +103,18 @@ export class Evaluator {
|
|||
|
||||
switch (node.kind) {
|
||||
|
||||
case SyntaxKind.SourceFile:
|
||||
case SyntaxKind.Module:
|
||||
for (const element of node.elements) {
|
||||
this.eval(element);
|
||||
}
|
||||
break;
|
||||
|
||||
case SyntaxKind.NewTypeDecl:
|
||||
case SyntaxKind.RecordDecl:
|
||||
case SyntaxKind.FuncDecl:
|
||||
break;
|
||||
|
||||
case SyntaxKind.MatchExpr:
|
||||
const value = this.eval(node.value);
|
||||
for (const [pattern, result] of node.arms) {
|
||||
|
|
|
@ -129,7 +129,8 @@ export class Expander {
|
|||
return node;
|
||||
}
|
||||
|
||||
return new Module(node.isPublic, node.name, node.elements, null, node);
|
||||
return new Module(node.isPublic, node.name, expanded, null, node);
|
||||
|
||||
|
||||
} else if (node.kind === SyntaxKind.Sentence) {
|
||||
|
||||
|
|
14
src/package.ts
Normal file
14
src/package.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
import { TextFile } from "./ast"
|
||||
|
||||
export class Package {
|
||||
|
||||
constructor(
|
||||
public name: string | null,
|
||||
public files: TextFile[],
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
150
src/parser.ts
150
src/parser.ts
|
@ -25,6 +25,7 @@ import {
|
|||
SourceElement,
|
||||
Module,
|
||||
RecordDecl,
|
||||
NewTypeDecl,
|
||||
} from "./ast"
|
||||
|
||||
import { stringType, intType } from "./checker"
|
||||
|
@ -55,6 +56,8 @@ function describeKind(kind: SyntaxKind): string {
|
|||
return "':'"
|
||||
case SyntaxKind.Dot:
|
||||
return "'.'"
|
||||
case SyntaxKind.RArrow:
|
||||
return "'->'"
|
||||
case SyntaxKind.Comma:
|
||||
return "','"
|
||||
case SyntaxKind.ModKeyword:
|
||||
|
@ -91,8 +94,52 @@ export class ParseError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
enum OperatorKind {
|
||||
Prefix,
|
||||
InfixL,
|
||||
InfixR,
|
||||
Suffix,
|
||||
}
|
||||
|
||||
interface OperatorInfo {
|
||||
kind: OperatorKind;
|
||||
arity: number;
|
||||
name: string;
|
||||
precedence: number;
|
||||
}
|
||||
|
||||
export class Parser {
|
||||
|
||||
operatorTable = [
|
||||
[
|
||||
[OperatorKind.InfixL, 2, '&&'],
|
||||
[OperatorKind.InfixL, 2, '||']
|
||||
],
|
||||
[
|
||||
[OperatorKind.InfixL, 2, '<'],
|
||||
[OperatorKind.InfixL, 2, '>'],
|
||||
[OperatorKind.InfixL, 2, '<='],
|
||||
[OperatorKind.InfixL, 2, '>=']
|
||||
],
|
||||
[
|
||||
[OperatorKind.InfixL, 2, '>>'],
|
||||
[OperatorKind.InfixL, 2, '<<']
|
||||
],
|
||||
[
|
||||
[OperatorKind.InfixL, 2, '+'],
|
||||
[OperatorKind.InfixL, 2, '-'],
|
||||
],
|
||||
[
|
||||
[OperatorKind.InfixL, 2, '/'],
|
||||
[OperatorKind.InfixL, 2, '*'],
|
||||
[OperatorKind.InfixL, 2, '%'],
|
||||
],
|
||||
[
|
||||
[OperatorKind.Prefix, '!']
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
parseQualName(tokens: TokenStream): QualName {
|
||||
|
||||
const path: Identifier[] = [];
|
||||
|
@ -171,7 +218,23 @@ export class Parser {
|
|||
// Assuming first token is 'syntax'
|
||||
tokens.get();
|
||||
|
||||
throw new Error('not implemented')
|
||||
const t1 = tokens.get();
|
||||
if (t1.kind !== SyntaxKind.Braced) {
|
||||
throw new ParseError(t1, [SyntaxKind.Braced])
|
||||
}
|
||||
|
||||
const innerTokens = t1.toTokenStream();
|
||||
|
||||
const pattern = this.parsePattern(innerTokens)
|
||||
|
||||
const t2 = innerTokens.get();
|
||||
if (t2.kind !== SyntaxKind.RArrow) {
|
||||
throw new ParseError(t2, [SyntaxKind.RArrow]);
|
||||
}
|
||||
|
||||
const body = this.parseBody(innerTokens);
|
||||
|
||||
return new Macro(pattern, body)
|
||||
|
||||
}
|
||||
|
||||
|
@ -255,7 +318,7 @@ export class Parser {
|
|||
}
|
||||
|
||||
parseStmt(tokens: TokenStream): Stmt {
|
||||
|
||||
this.parseCallExpr(tokens)
|
||||
}
|
||||
|
||||
parseRecordDecl(tokens: TokenStream): RecordDecl {
|
||||
|
@ -329,6 +392,34 @@ export class Parser {
|
|||
}
|
||||
}
|
||||
|
||||
parseNewType(tokens: TokenSteam): NewTypeDecl {
|
||||
|
||||
let isPublic = false;
|
||||
let t0 = tokens.get();
|
||||
if (t0.kind !== SyntaxKind.Identifier) {
|
||||
throw new ParseError(t0, [SyntaxKind.PubKeyword, SyntaxKind.NewTypeKeyword])
|
||||
}
|
||||
if (t0.text === 'pub') {
|
||||
isPublic = true;
|
||||
t0 = tokens.get();
|
||||
if (t0.kind !== SyntaxKind.Identifier) {
|
||||
throw new ParseError(t0, [SyntaxKind.NewTypeKeyword])
|
||||
}
|
||||
}
|
||||
|
||||
if (t0.text !== 'newtype') {
|
||||
throw new ParseError(t0, [SyntaxKind.NewTypeKeyword])
|
||||
}
|
||||
|
||||
const name = tokens.get();
|
||||
if (name.kind !== SyntaxKind.Identifier) {
|
||||
throw new ParseError(name, [SyntaxKind.Identifier])
|
||||
}
|
||||
|
||||
return new NewTypeDecl(isPublic, name)
|
||||
|
||||
}
|
||||
|
||||
parseFuncDecl(tokens: TokenStream, origNode: Syntax | null): FuncDecl {
|
||||
|
||||
let target = "Bolt";
|
||||
|
@ -489,6 +580,10 @@ export class Parser {
|
|||
}
|
||||
}
|
||||
switch (kw.text) {
|
||||
case 'newtype':
|
||||
return this.parseNewType(tokens);
|
||||
case 'syntax':
|
||||
return this.parseSyntax(tokens);
|
||||
case 'mod':
|
||||
return this.parseModDecl(tokens);
|
||||
case 'fn':
|
||||
|
@ -500,13 +595,62 @@ export class Parser {
|
|||
case 'enum':
|
||||
return this.parseVariantDecl(tokens);
|
||||
default:
|
||||
throw new ParseError(kw, [SyntaxKind.ModKeyword, SyntaxKind.LetKeyword, SyntaxKind.FnKeyword, SyntaxKind.EnumKeyword, SyntaxKind.StructKeyword])
|
||||
try {
|
||||
return this.parseExpr(tokens)
|
||||
} catch (e) {
|
||||
if (e instanceof ParseError) {
|
||||
throw new ParseError(kw, [...e.expected, SyntaxKind.ModKeyword, SyntaxKind.LetKeyword, SyntaxKind.FnKeyword, SyntaxKind.EnumKeyword, SyntaxKind.StructKeyword])
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return this.parseStmt(tokens)
|
||||
}
|
||||
}
|
||||
|
||||
getOperatorDesc(seekArity: number, seekName: string): OperatorInfo {
|
||||
for (let i = 0; i < this.operatorTable.length; ++i) {
|
||||
for (const [kind, arity, name] of this.operatorTable[i]) {
|
||||
if (artity == seekArity && name === seekName) {
|
||||
return {
|
||||
kind,
|
||||
name,
|
||||
arity,
|
||||
precedence: i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseBinOp(tokens: TokenStream, lhs: Expr , minPrecedence: number) {
|
||||
let lookahead = tokens.peek(1);
|
||||
while (true) {
|
||||
if (lookahead.kind !== SyntaxKind.Operator) {
|
||||
break;
|
||||
}
|
||||
const lookaheadDesc = this.getOperatorDesc(2, lookahead.text);
|
||||
if (lookaheadDesc === null || lookaheadDesc.precedence < minPrecedence) {
|
||||
break;
|
||||
}
|
||||
const op = lookahead;
|
||||
const opDesc = this.getOperatorDesc(2, op.text);
|
||||
tokens.get();
|
||||
let rhs = this.parsePrimExpr(tokens)
|
||||
lookahead = tokens.peek()
|
||||
while (lookaheadDesc.arity === 2
|
||||
&& ((lookaheadDesc.precedence > opDesc.precedence)
|
||||
|| lookaheadDesc.kind === OperatorKind.InfixR && lookaheadDesc.precedence === opDesc.precedence)) {
|
||||
rhs = this.parseBinOp(tokens, rhs, lookaheadDesc.precedence)
|
||||
}
|
||||
lookahead = tokens.peek();
|
||||
lhs = new CallExpr(new RefExpr(new QualName(op, [])), [lhs, rhs]);
|
||||
}
|
||||
return lhs
|
||||
}
|
||||
|
||||
parseCallExpr(tokens: TokenStream): CallExpr {
|
||||
|
||||
const operator = this.parsePrimExpr(tokens)
|
||||
|
|
56
src/program.ts
Normal file
56
src/program.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
|
||||
import { FastStringMap } from "./util"
|
||||
import { Parser } from "./parser"
|
||||
import { TypeChecker } from "./checker"
|
||||
import { Evaluator } from "./evaluator"
|
||||
import { Expander } from "./expander"
|
||||
import { Scanner } from "./scanner"
|
||||
import { Compiler } from "./compiler"
|
||||
import { TextFile, SourceFile } from "./ast"
|
||||
|
||||
export class Program {
|
||||
|
||||
parser: Parser
|
||||
evaluator: Evaluator;
|
||||
checker: TypeChecker;
|
||||
expander: Expander;
|
||||
|
||||
sourceFiles = new Map<TextFile, SourceFile>();
|
||||
|
||||
constructor(public files: TextFile[]) {
|
||||
this.checker = new TypeChecker();
|
||||
this.parser = new Parser();
|
||||
this.evaluator = new Evaluator(this.checker);
|
||||
this.expander = new Expander(this.parser, this.evaluator, this.checker);
|
||||
for (const file of files) {
|
||||
const contents = fs.readFileSync(file.fullPath, 'utf8');
|
||||
const scanner = new Scanner(file, contents)
|
||||
this.sourceFiles.set(file, scanner.scan());
|
||||
}
|
||||
}
|
||||
|
||||
compile(file: TextFile) {
|
||||
const original = this.sourceFiles.get(file);
|
||||
if (original === undefined) {
|
||||
throw new Error(`File ${file.path} does not seem to be part of this Program.`)
|
||||
}
|
||||
const expanded = this.expander.getFullyExpanded(original) as SourceFile;
|
||||
const compiler = new Compiler(this.checker, { target: "JS" })
|
||||
const compiled = compiler.compile(expanded)
|
||||
return compiled
|
||||
}
|
||||
|
||||
eval(file: TextFile) {
|
||||
const original = this.sourceFiles.get(file);
|
||||
if (original === undefined) {
|
||||
throw new Error(`File ${file.path} does not seem to be part of this Program.`)
|
||||
}
|
||||
const expanded = this.expander.getFullyExpanded(original) as SourceFile;
|
||||
return this.evaluator.eval(expanded)
|
||||
}
|
||||
|
||||
}
|
||||
|
0
stdlib/Boltfile
Normal file
0
stdlib/Boltfile
Normal file
24
stdlib/lang/bolt.bolt
Normal file
24
stdlib/lang/bolt.bolt
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
mod Lang.Bolt.AST {
|
||||
|
||||
pub struct Pos {
|
||||
offset: Int,
|
||||
line: Int,
|
||||
column: Int,
|
||||
}
|
||||
|
||||
pub struct Span {
|
||||
file: File,
|
||||
start: TextPos,
|
||||
end: TextPos,
|
||||
}
|
||||
|
||||
pub struct Identifier {
|
||||
text: String,
|
||||
span: Option<Span>,
|
||||
orig_node: Option<Node>,
|
||||
parent: Option<Node>,
|
||||
}
|
||||
|
||||
}
|
||||
|
0
stdlib/main.bolt
Normal file
0
stdlib/main.bolt
Normal file
17
stdlib/math.bolt
Normal file
17
stdlib/math.bolt
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
newtype Int;
|
||||
|
||||
pub fn fac(n: Int) -> Int {
|
||||
if n == 0 {
|
||||
return 1
|
||||
} else {
|
||||
return fac(n-1)
|
||||
}
|
||||
}
|
||||
|
||||
fn (a: Int) + (b: Int) -> Int {
|
||||
|
||||
}
|
||||
|
||||
precedence a + b < a * b;
|
||||
|
11
stdlib/syntax.bolt
Normal file
11
stdlib/syntax.bolt
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
syntax {
|
||||
quote {
|
||||
macro $name: QualName {
|
||||
|
||||
}
|
||||
} => {
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue