This commit is contained in:
Sam Vervaeck 2020-03-03 14:53:54 +01:00
parent 75e5928f9e
commit f1d35917f2
15 changed files with 495 additions and 204 deletions

View file

@ -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

View file

@ -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

View file

@ -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.`)
}
}

View file

@ -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)
// }

View file

@ -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({

View file

@ -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) {

View file

@ -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
View file

@ -0,0 +1,14 @@
import { TextFile } from "./ast"
export class Package {
constructor(
public name: string | null,
public files: TextFile[],
) {
}
}

View file

@ -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
View 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
View file

24
stdlib/lang/bolt.bolt Normal file
View 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
View file

17
stdlib/math.bolt Normal file
View 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
View file

@ -0,0 +1,11 @@
syntax {
quote {
macro $name: QualName {
}
} => {
}
}