js-slang
Version:
Javascript-based implementations of Source, written in Typescript
927 lines • 45.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemeParser = void 0;
const token_type_1 = require("../types/tokens/token-type");
const location_1 = require("../types/location");
const scheme_node_types_1 = require("../types/nodes/scheme-node-types");
const ParserError = require("./parser-error");
const group_1 = require("../types/tokens/group");
const tokens_1 = require("../types/tokens");
/**
* An enum representing the current quoting mode of the parser
*/
var QuoteMode;
(function (QuoteMode) {
QuoteMode[QuoteMode["NONE"] = 0] = "NONE";
QuoteMode[QuoteMode["QUOTE"] = 1] = "QUOTE";
QuoteMode[QuoteMode["QUASIQUOTE"] = 2] = "QUASIQUOTE";
})(QuoteMode || (QuoteMode = {}));
class SchemeParser {
constructor(source, tokens, chapter = Infinity) {
this.current = 0;
this.quoteMode = QuoteMode.NONE;
// We can group syntactical elements by their chapter
this.BASIC_CHAPTER = 1;
this.QUOTING_CHAPTER = 2;
this.VECTOR_CHAPTER = 3;
this.MUTABLE_CHAPTER = 3;
this.source = source;
this.tokens = tokens;
this.chapter = chapter;
}
advance() {
if (!this.isAtEnd())
this.current++;
return this.previous();
}
isAtEnd() {
return this.current >= this.tokens.length;
}
previous() {
return this.tokens[this.current - 1];
}
peek() {
return this.tokens[this.current];
}
validateChapter(c, chapter) {
if (this.chapter < chapter) {
throw new ParserError.DisallowedTokenError(this.source, c.pos, c, this.chapter);
}
}
/**
* Returns the location of a token.
* @param token A token.
* @returns The location of the token.
*/
toLocation(token) {
return new location_1.Location(token.pos, token.endPos);
}
/**
* Helper function used to destructure a list into its elements and terminator.
* An optional verifier is used if there are restrictions on the elements of the list.
*/
destructureList(list, verifier = (_x) => { }) {
// check if the list is an empty list
if (list.length === 0) {
return [[], undefined];
}
// check if the list is a list of length 1
if (list.length === 1) {
verifier(list[0]);
return [[this.parseExpression(list[0])], undefined];
}
// we now know that the list is at least of length 2
// check for a dotted list
// it is if the second last element is a dot
const potentialDot = list.at(-2);
if ((0, tokens_1.isToken)(potentialDot) && potentialDot.type === token_type_1.TokenType.DOT) {
const cdrElement = list.at(-1);
const listElements = list.slice(0, -2);
verifier(cdrElement);
listElements.forEach(verifier);
return [
listElements.map(this.parseExpression.bind(this)),
this.parseExpression(cdrElement),
];
}
// we now know that it is a proper list
const listElements = list;
listElements.forEach(verifier);
return [listElements.map(this.parseExpression.bind(this)), undefined];
}
/**
* Returns a group of associated tokens.
* Tokens are grouped by level of parentheses.
*
* @param openparen The opening parenthesis, if one exists.
* @returns A group of tokens or groups of tokens.
*/
grouping(openparen) {
const elements = [];
let inList = false;
if (openparen) {
inList = true;
elements.push(openparen);
}
do {
let c = this.advance();
switch (c.type) {
case token_type_1.TokenType.LEFT_PAREN:
case token_type_1.TokenType.LEFT_BRACKET:
// the next group is not empty, especially because it
// has an open parenthesis
const innerGroup = this.grouping(c);
elements.push(innerGroup);
break;
case token_type_1.TokenType.RIGHT_PAREN:
case token_type_1.TokenType.RIGHT_BRACKET:
if (!inList) {
throw new ParserError.UnexpectedFormError(this.source, c.pos, c);
}
// add the parenthesis to the current group
elements.push(c);
inList = false;
break;
case token_type_1.TokenType.APOSTROPHE: // Quoting syntax (short form)
case token_type_1.TokenType.BACKTICK:
case token_type_1.TokenType.COMMA:
case token_type_1.TokenType.COMMA_AT:
case token_type_1.TokenType.HASH_VECTOR: // Vector syntax
// these cases modify only the next element
// so we group up the next element and use this
// token on it
let nextGrouping;
do {
nextGrouping = this.grouping();
} while (!nextGrouping);
elements.push(this.affect(c, nextGrouping));
break;
case token_type_1.TokenType.QUOTE: // Quoting syntax
case token_type_1.TokenType.QUASIQUOTE:
case token_type_1.TokenType.UNQUOTE:
case token_type_1.TokenType.UNQUOTE_SPLICING:
case token_type_1.TokenType.IDENTIFIER: // Atomics
case token_type_1.TokenType.NUMBER:
case token_type_1.TokenType.BOOLEAN:
case token_type_1.TokenType.STRING:
case token_type_1.TokenType.DOT:
case token_type_1.TokenType.DEFINE: // Chapter 1
case token_type_1.TokenType.IF:
case token_type_1.TokenType.ELSE:
case token_type_1.TokenType.COND:
case token_type_1.TokenType.LAMBDA:
case token_type_1.TokenType.LET:
case token_type_1.TokenType.SET: // Chapter 3
case token_type_1.TokenType.BEGIN:
case token_type_1.TokenType.DELAY:
case token_type_1.TokenType.IMPORT:
case token_type_1.TokenType.EXPORT:
elements.push(c);
break;
case token_type_1.TokenType.HASH_SEMICOLON:
// a datum comment
// get the next NON-EMPTY grouping
// and ignore it
while (!this.grouping()) { }
break;
case token_type_1.TokenType.EOF:
// We should be unable to reach this point at top level as parse()
// should prevent the grouping of the singular EOF token.
// However, with any element that ranges beyond the end of the
// file without its corresponding delemiter, we can reach this point.
throw new ParserError.UnexpectedEOFError(this.source, c.pos);
default:
throw new ParserError.UnexpectedFormError(this.source, c.pos, c);
}
} while (inList);
if (elements.length === 0) {
return;
}
try {
return group_1.Group.build(elements);
}
catch (e) {
if (e instanceof ParserError.ExpectedFormError) {
throw new ParserError.ExpectedFormError(this.source, e.loc, e.form, e.expected);
}
throw e;
}
}
/**
* Groups an affector token with its target.
*/
affect(affector, target) {
return group_1.Group.build([affector, target]);
}
/**
* Parse an expression.
* @param expr A token or a group of tokens.
* @returns
*/
parseExpression(expr) {
// Discern the type of expression
if ((0, tokens_1.isToken)(expr)) {
return this.parseToken(expr);
}
// We now know it is a group
// Due to group invariants we can determine if it represents a
// single token instead
if (expr.isSingleIdentifier()) {
return this.parseToken(expr.unwrap()[0]);
}
return this.parseGroup(expr);
}
parseToken(token) {
switch (token.type) {
case token_type_1.TokenType.IDENTIFIER:
return this.quoteMode === QuoteMode.NONE
? new scheme_node_types_1.Atomic.Identifier(this.toLocation(token), token.lexeme)
: new scheme_node_types_1.Atomic.Symbol(this.toLocation(token), token.lexeme);
// all of these are self evaluating, and so can be left alone regardless of quote mode
case token_type_1.TokenType.NUMBER:
return new scheme_node_types_1.Atomic.NumericLiteral(this.toLocation(token), token.literal);
case token_type_1.TokenType.BOOLEAN:
return new scheme_node_types_1.Atomic.BooleanLiteral(this.toLocation(token), token.literal);
case token_type_1.TokenType.STRING:
return new scheme_node_types_1.Atomic.StringLiteral(this.toLocation(token), token.literal);
default:
// if in a quoting context, any keyword is instead treated as a symbol
if (this.quoteMode !== QuoteMode.NONE) {
return new scheme_node_types_1.Atomic.Symbol(this.toLocation(token), token.lexeme);
}
throw new ParserError.UnexpectedFormError(this.source, token.pos, token);
}
}
parseGroup(group) {
// No need to check if group represents a single token as well
if (!group.isParenthesized()) {
// The only case left is the unparenthesized case
// of a single affector token and a target group
// Form: <affector token> <group>
return this.parseAffectorGroup(group);
}
// Now we have fallen through to the generic group
// case - a parenthesized group of tokens.
switch (this.quoteMode) {
case QuoteMode.NONE:
return this.parseNormalGroup(group);
case QuoteMode.QUOTE:
case QuoteMode.QUASIQUOTE:
return this.parseQuotedGroup(group);
}
}
/**
* Parse a group of tokens affected by an affector.
* Important case as affector changes quotation mode.
*
* @param group A group of tokens, verified to be an affector and a target.
* @returns An expression.
*/
parseAffectorGroup(group) {
const [affector, target] = group.unwrap();
// Safe to cast affector due to group invariants
switch (affector.type) {
case token_type_1.TokenType.APOSTROPHE:
case token_type_1.TokenType.QUOTE:
this.validateChapter(affector, this.QUOTING_CHAPTER);
if (this.quoteMode !== QuoteMode.NONE) {
const innerGroup = this.parseExpression(target);
const newSymbol = new scheme_node_types_1.Atomic.Symbol(this.toLocation(affector), "quote");
const newLocation = newSymbol.location.merge(innerGroup.location);
// wrap the entire expression in a list
return new scheme_node_types_1.Extended.List(newLocation, [newSymbol, innerGroup]);
}
this.quoteMode = QuoteMode.QUOTE;
const quotedExpression = this.parseExpression(target);
this.quoteMode = QuoteMode.NONE;
return quotedExpression;
case token_type_1.TokenType.BACKTICK:
case token_type_1.TokenType.QUASIQUOTE:
this.validateChapter(affector, this.QUOTING_CHAPTER);
if (this.quoteMode !== QuoteMode.NONE) {
const innerGroup = this.parseExpression(target);
const newSymbol = new scheme_node_types_1.Atomic.Symbol(this.toLocation(affector), "quasiquote");
const newLocation = newSymbol.location.merge(innerGroup.location);
// wrap the entire expression in a list
return new scheme_node_types_1.Extended.List(newLocation, [newSymbol, innerGroup]);
}
this.quoteMode = QuoteMode.QUASIQUOTE;
const quasiquotedExpression = this.parseExpression(target);
this.quoteMode = QuoteMode.NONE;
return quasiquotedExpression;
case token_type_1.TokenType.COMMA:
case token_type_1.TokenType.UNQUOTE:
this.validateChapter(affector, this.QUOTING_CHAPTER);
let preUnquoteMode = this.quoteMode;
if (preUnquoteMode === QuoteMode.NONE) {
throw new ParserError.UnsupportedTokenError(this.source, affector.pos, affector);
}
if (preUnquoteMode === QuoteMode.QUOTE) {
const innerGroup = this.parseExpression(target);
const newSymbol = new scheme_node_types_1.Atomic.Symbol(this.toLocation(affector), "unquote");
const newLocation = newSymbol.location.merge(innerGroup.location);
// wrap the entire expression in a list
return new scheme_node_types_1.Extended.List(newLocation, [newSymbol, innerGroup]);
}
this.quoteMode = QuoteMode.NONE;
const unquotedExpression = this.parseExpression(target);
this.quoteMode = preUnquoteMode;
return unquotedExpression;
case token_type_1.TokenType.COMMA_AT:
case token_type_1.TokenType.UNQUOTE_SPLICING:
// Unquote-splicing will be evaluated at runtime,
// Proper unquote splicing will be dealt with in semester 2.
this.validateChapter(affector, this.QUOTING_CHAPTER);
let preUnquoteSplicingMode = this.quoteMode;
if (preUnquoteSplicingMode === QuoteMode.NONE) {
throw new ParserError.UnexpectedFormError(this.source, affector.pos, affector);
}
if (preUnquoteSplicingMode === QuoteMode.QUOTE) {
const innerGroup = this.parseExpression(target);
const newSymbol = new scheme_node_types_1.Atomic.Symbol(this.toLocation(affector), "unquote-splicing");
const newLocation = newSymbol.location.merge(innerGroup.location);
// wrap the entire expression in a list
return new scheme_node_types_1.Extended.List(newLocation, [newSymbol, innerGroup]);
}
throw new ParserError.UnsupportedTokenError(this.source, affector.pos, affector);
this.quoteMode = QuoteMode.NONE;
const unquoteSplicedExpression = this.parseExpression(target);
this.quoteMode = preUnquoteSplicingMode;
const newLocation = this.toLocation(affector).merge(unquoteSplicedExpression.location);
return new scheme_node_types_1.Atomic.SpliceMarker(newLocation, unquoteSplicedExpression);
case token_type_1.TokenType.HASH_VECTOR:
// vectors quote over all elements inside.
this.validateChapter(affector, this.VECTOR_CHAPTER);
let preVectorQuoteMode = this.quoteMode;
this.quoteMode = QuoteMode.QUOTE;
const vector = this.parseVector(group);
this.quoteMode = preVectorQuoteMode;
return vector;
default:
throw new ParserError.UnexpectedFormError(this.source, affector.pos, affector);
}
}
parseNormalGroup(group) {
// it is an error if the group is empty in a normal context
if (group.length() === 0) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "non-empty group");
}
// get the first element
const firstElement = group.unwrap()[0];
// If the first element is a token, it may be a keyword or a procedure call
if ((0, tokens_1.isToken)(firstElement)) {
switch (firstElement.type) {
// Scheme chapter 1
case token_type_1.TokenType.LAMBDA:
this.validateChapter(firstElement, this.BASIC_CHAPTER);
return this.parseLambda(group);
case token_type_1.TokenType.DEFINE:
this.validateChapter(firstElement, this.BASIC_CHAPTER);
return this.parseDefinition(group);
case token_type_1.TokenType.IF:
this.validateChapter(firstElement, this.BASIC_CHAPTER);
return this.parseConditional(group);
case token_type_1.TokenType.LET:
this.validateChapter(firstElement, this.BASIC_CHAPTER);
return this.parseLet(group);
case token_type_1.TokenType.COND:
this.validateChapter(firstElement, this.BASIC_CHAPTER);
return this.parseExtendedCond(group);
// Scheme chapter 2
case token_type_1.TokenType.QUOTE:
case token_type_1.TokenType.APOSTROPHE:
case token_type_1.TokenType.QUASIQUOTE:
case token_type_1.TokenType.BACKTICK:
case token_type_1.TokenType.UNQUOTE:
case token_type_1.TokenType.COMMA:
case token_type_1.TokenType.UNQUOTE_SPLICING:
case token_type_1.TokenType.COMMA_AT:
this.validateChapter(firstElement, this.QUOTING_CHAPTER);
// we can reuse the affector group method to control the quote mode
return this.parseAffectorGroup(group);
// Scheme chapter 3
case token_type_1.TokenType.BEGIN:
this.validateChapter(firstElement, this.MUTABLE_CHAPTER);
return this.parseBegin(group);
case token_type_1.TokenType.DELAY:
this.validateChapter(firstElement, this.MUTABLE_CHAPTER);
return this.parseDelay(group);
case token_type_1.TokenType.SET:
this.validateChapter(firstElement, this.MUTABLE_CHAPTER);
return this.parseSet(group);
// Scm-slang misc
case token_type_1.TokenType.IMPORT:
this.validateChapter(firstElement, this.BASIC_CHAPTER);
return this.parseImport(group);
case token_type_1.TokenType.EXPORT:
this.validateChapter(firstElement, this.BASIC_CHAPTER);
return this.parseExport(group);
case token_type_1.TokenType.VECTOR:
this.validateChapter(firstElement, this.VECTOR_CHAPTER);
// same as above, this is an affector group
return this.parseAffectorGroup(group);
default:
// It's a procedure call
return this.parseApplication(group);
}
}
// Form: (<group> <expr>*)
// It's a procedure call
return this.parseApplication(group);
}
/**
* We are parsing a list/dotted list.
*/
parseQuotedGroup(group) {
// check if the group is an empty list
if (group.length() === 0) {
return new scheme_node_types_1.Atomic.Nil(group.location);
}
// check if the group is a list of length 1
if (group.length() === 1) {
const elem = [this.parseExpression(group.unwrap()[0])];
return new scheme_node_types_1.Extended.List(group.location, elem);
}
// we now know that the group is at least of length 2
const groupElements = group.unwrap();
const [listElements, cdrElement] = this.destructureList(groupElements);
return new scheme_node_types_1.Extended.List(group.location, listElements, cdrElement);
}
// _____________________CHAPTER 1_____________________
/**
* Parse a lambda expression.
* @param group
* @returns
*/
parseLambda(group) {
// Form: (lambda (<identifier>*) <body>+)
// | (lambda (<identifier>* . <rest-identifier>) <body>+)
// ensure that the group has at least 3 elements
if (group.length() < 3) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(lambda (<identifier>* . <rest-identifier>?) <body>+) | (lambda <rest-identifer> <body>+)");
}
const elements = group.unwrap();
const formals = elements[1];
const body = elements.slice(2);
// Formals should be a group of identifiers or a single identifier
let convertedFormals = [];
// if a rest element is detected,
let convertedRest = undefined;
if ((0, tokens_1.isToken)(formals)) {
if (formals.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, formals.pos, formals, "<rest-identifier>");
}
convertedRest = new scheme_node_types_1.Atomic.Identifier(this.toLocation(formals), formals.lexeme);
}
else {
// it is a group
const formalsElements = formals.unwrap();
[convertedFormals, convertedRest] = this.destructureList(formalsElements,
// pass in a verifier that checks if the elements are identifiers
formal => {
if (!(0, tokens_1.isToken)(formal)) {
throw new ParserError.ExpectedFormError(this.source, formal.pos, formal, "<identifier>");
}
if (formal.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, formal.pos, formal, "<identifier>");
}
});
}
// Body is treated as a group of expressions
const convertedBody = body.map(this.parseExpression.bind(this));
// assert that body is not empty
if (convertedBody.length < 1) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(lambda ... <body>+)");
}
if (convertedBody.length === 1) {
return new scheme_node_types_1.Atomic.Lambda(group.location, convertedBody[0], convertedFormals, convertedRest);
}
const newLocation = convertedBody
.at(0)
.location.merge(convertedBody.at(-1).location);
const bodySequence = new scheme_node_types_1.Atomic.Sequence(newLocation, convertedBody);
return new scheme_node_types_1.Atomic.Lambda(group.location, bodySequence, convertedFormals, convertedRest);
}
/**
* Parse a define expression.
* @param group
* @returns
*/
parseDefinition(group) {
// Form: (define <identifier> <expr>)
// | (define (<identifier> <formals>) <body>)
// | (define (<identifier> <formals>) <body> <body>*)
// ensure that the group has at least 3 elements
if (group.length() < 3) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(define <identifier> <expr>) | (define (<identifier> <formals>) <body>+)");
}
const elements = group.unwrap();
const identifier = elements[1];
const expr = elements.slice(2);
let convertedIdentifier;
let convertedFormals = [];
let convertedRest = undefined;
let isFunctionDefinition = false;
// Identifier may be a token or a group of identifiers
if ((0, tokens_1.isGroup)(identifier)) {
// its a function definition
isFunctionDefinition = true;
const identifierElements = identifier.unwrap();
const functionName = identifierElements[0];
const formals = identifierElements.splice(1);
// verify that the first element is an identifier
if (!(0, tokens_1.isToken)(functionName)) {
throw new ParserError.ExpectedFormError(this.source, functionName.location.start, functionName, "<identifier>");
}
if (functionName.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, functionName.pos, functionName, "<identifier>");
}
// convert the first element to an identifier
convertedIdentifier = new scheme_node_types_1.Atomic.Identifier(this.toLocation(functionName), functionName.lexeme);
// Formals should be a group of identifiers
[convertedFormals, convertedRest] = this.destructureList(formals, formal => {
if (!(0, tokens_1.isToken)(formal)) {
throw new ParserError.ExpectedFormError(this.source, formal.pos, formal, "<identifier>");
}
if (formal.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, formal.pos, formal, "<identifier>");
}
});
}
else if (identifier.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, identifier.pos, identifier, "<identifier>");
}
else {
// its a normal definition
convertedIdentifier = new scheme_node_types_1.Atomic.Identifier(this.toLocation(identifier), identifier.lexeme);
isFunctionDefinition = false;
}
// expr cannot be empty
if (expr.length < 1) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(define ... <body>+)");
}
if (isFunctionDefinition) {
// Body is treated as a group of expressions
const convertedBody = expr.map(this.parseExpression.bind(this));
if (convertedBody.length === 1) {
return new scheme_node_types_1.Extended.FunctionDefinition(group.location, convertedIdentifier, convertedBody[0], convertedFormals, convertedRest);
}
const newLocation = convertedBody
.at(0)
.location.merge(convertedBody.at(-1).location);
const bodySequence = new scheme_node_types_1.Atomic.Sequence(newLocation, convertedBody);
return new scheme_node_types_1.Extended.FunctionDefinition(group.location, convertedIdentifier, bodySequence, convertedFormals, convertedRest);
}
// its a normal definition
if (expr.length > 1) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(define <identifier> <expr>)");
}
// Expr is treated as a single expression
const convertedExpr = this.parseExpression(expr[0]);
return new scheme_node_types_1.Atomic.Definition(group.location, convertedIdentifier, convertedExpr);
}
/**
* Parse a conditional expression.
* @param group
* @returns
*/
parseConditional(group) {
// Form: (if <pred> <cons> <alt>)
// | (if <pred> <cons>)
// ensure that the group has 3 or 4 elements
if (group.length() < 3 || group.length() > 4) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(if <pred> <cons> <alt>?)");
}
const elements = group.unwrap();
const test = elements[1];
const consequent = elements[2];
const alternate = group.length() > 3 ? elements[3] : undefined;
// Test is treated as a single expression
const convertedTest = this.parseExpression(test);
// Consequent is treated as a single expression
const convertedConsequent = this.parseExpression(consequent);
// Alternate is treated as a single expression
const convertedAlternate = alternate
? this.parseExpression(alternate)
: new scheme_node_types_1.Atomic.Identifier(group.location, "undefined");
return new scheme_node_types_1.Atomic.Conditional(group.location, convertedTest, convertedConsequent, convertedAlternate);
}
/**
* Parse an application expression.
*/
parseApplication(group) {
// Form: (<func> <args>*)
// ensure that the group has at least 1 element
if (group.length() < 1) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(<func> <args>*)");
}
const elements = group.unwrap();
const operator = elements[0];
const operands = elements.splice(1);
// Operator is treated as a single expression
const convertedOperator = this.parseExpression(operator);
// Operands are treated as a group of expressions
const convertedOperands = [];
for (const operand of operands) {
convertedOperands.push(this.parseExpression(operand));
}
return new scheme_node_types_1.Atomic.Application(group.location, convertedOperator, convertedOperands);
}
/**
* Parse a let expression.
* @param group
* @returns
*/
parseLet(group) {
// Form: (let ((<identifier> <value>)*) <body>+)
// ensure that the group has at least 3 elements
if (group.length() < 3) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(let ((<identifier> <value>)*) <body>+)");
}
const elements = group.unwrap();
const bindings = elements[1];
const body = elements.slice(2);
// Verify bindings is a group
if (!(0, tokens_1.isGroup)(bindings)) {
throw new ParserError.ExpectedFormError(this.source, bindings.pos, bindings, "((<identifier> <value>)*)");
}
// Bindings are treated as a group of grouped identifiers and values
const convertedIdentifiers = [];
const convertedValues = [];
const bindingElements = bindings.unwrap();
for (const bindingElement of bindingElements) {
// Verify bindingElement is a group of size 2
if (!(0, tokens_1.isGroup)(bindingElement)) {
throw new ParserError.ExpectedFormError(this.source, bindingElement.pos, bindingElement, "(<identifier> <value>)");
}
if (bindingElement.length() !== 2) {
throw new ParserError.ExpectedFormError(this.source, bindingElement.location.start, bindingElement, "(<identifier> <value>)");
}
const [identifier, value] = bindingElement.unwrap();
// Verify identifier is a token and an identifier
if (!(0, tokens_1.isToken)(identifier)) {
throw new ParserError.ExpectedFormError(this.source, identifier.location.start, identifier, "<identifier>");
}
if (identifier.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, identifier.pos, identifier, "<identifier>");
}
convertedIdentifiers.push(new scheme_node_types_1.Atomic.Identifier(this.toLocation(identifier), identifier.lexeme));
convertedValues.push(this.parseExpression(value));
}
// Body is treated as a group of expressions
const convertedBody = body.map(this.parseExpression.bind(this));
// assert that body is not empty
if (convertedBody.length < 1) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(let ... <body>+)");
}
if (convertedBody.length === 1) {
return new scheme_node_types_1.Extended.Let(group.location, convertedIdentifiers, convertedValues, convertedBody[0]);
}
const newLocation = convertedBody
.at(0)
.location.merge(convertedBody.at(-1).location);
const bodySequence = new scheme_node_types_1.Atomic.Sequence(newLocation, convertedBody);
return new scheme_node_types_1.Extended.Let(group.location, convertedIdentifiers, convertedValues, bodySequence);
}
/**
* Parse an extended cond expression.
* @param group
* @returns
*/
parseExtendedCond(group) {
// Form: (cond (<pred> <body>)*)
// | (cond (<pred> <body>)* (else <val>))
// ensure that the group has at least 2 elements
if (group.length() < 2) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(cond (<pred> <body>*)* (else <val>)?)");
}
const elements = group.unwrap();
const clauses = elements.splice(1);
// safe to cast because of the check above
const lastClause = clauses.pop();
// Clauses are treated as a group of groups of expressions
// Form: (<pred> <body>*)
const convertedClauses = [];
const convertedConsequents = [];
for (const clause of clauses) {
// Verify clause is a group with size no less than 1
if (!(0, tokens_1.isGroup)(clause)) {
throw new ParserError.ExpectedFormError(this.source, clause.pos, clause, "(<pred> <body>*)");
}
if (clause.length() < 1) {
throw new ParserError.ExpectedFormError(this.source, clause.firstToken().pos, clause.firstToken(), "(<pred> <body>*)");
}
const [test, ...consequent] = clause.unwrap();
// verify that test is NOT an else token
if ((0, tokens_1.isToken)(test) && test.type === token_type_1.TokenType.ELSE) {
throw new ParserError.ExpectedFormError(this.source, test.pos, test, "<predicate>");
}
// Test is treated as a single expression
const convertedTest = this.parseExpression(test);
// Consequent is treated as a group of expressions
const consequentExpressions = consequent.map(this.parseExpression.bind(this));
const consequentLocation = consequent.length < 1
? convertedTest.location
: consequentExpressions
.at(0)
.location.merge(consequentExpressions.at(-1).location);
// if consequent is empty, the test itself is treated
// as the value returned.
// if consequent is more than length one, there is a sequence.
const convertedConsequent = consequent.length < 1
? convertedTest
: consequent.length < 2
? consequentExpressions[0]
: new scheme_node_types_1.Atomic.Sequence(consequentLocation, consequentExpressions);
convertedClauses.push(convertedTest);
convertedConsequents.push(convertedConsequent);
}
// Check last clause
// Verify lastClause is a group with size at least 2
if (!(0, tokens_1.isGroup)(lastClause)) {
throw new ParserError.ExpectedFormError(this.source, lastClause.pos, lastClause, "(<pred> <body>+) | (else <val>)");
}
if (lastClause.length() < 2) {
throw new ParserError.ExpectedFormError(this.source, lastClause.firstToken().pos, lastClause.firstToken(), "(<pred> <body>+) | (else <val>)");
}
const [test, ...consequent] = lastClause.unwrap();
let isElse = false;
// verify that test is an else token
if ((0, tokens_1.isToken)(test) && test.type === token_type_1.TokenType.ELSE) {
isElse = true;
// verify that consequent is of length 1
if (consequent.length !== 1) {
throw new ParserError.ExpectedFormError(this.source, lastClause.location.start, lastClause, "(else <val>)");
}
}
// verify that consequent is at least 1 expression
if (consequent.length < 1) {
throw new ParserError.ExpectedFormError(this.source, lastClause.location.start, lastClause, "(<pred> <body>+)");
}
// Consequent is treated as a group of expressions
const consequentExpressions = consequent.map(this.parseExpression.bind(this));
const consequentLocation = consequentExpressions
.at(0)
.location.merge(consequentExpressions.at(-1).location);
const lastConsequent = consequent.length === 1
? consequentExpressions[0]
: new scheme_node_types_1.Atomic.Sequence(consequentLocation, consequentExpressions);
if (isElse) {
return new scheme_node_types_1.Extended.Cond(group.location, convertedClauses, convertedConsequents, lastConsequent);
}
// If the last clause is not an else clause, we treat it as a normal cond clause instead
const lastTest = this.parseExpression(test);
// Test
convertedClauses.push(lastTest);
convertedConsequents.push(lastConsequent);
return new scheme_node_types_1.Extended.Cond(group.location, convertedClauses, convertedConsequents);
}
// _____________________CHAPTER 3_____________________
/**
* Parse a reassignment expression.
* @param group
* @returns
*/
parseSet(group) {
// Form: (set! <identifier> <expr>)
// ensure that the group has 3 elements
if (group.length() !== 3) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(set! <identifier> <expr>)");
}
const elements = group.unwrap();
const identifier = elements[1];
const expr = elements[2];
// Identifier is treated as a single identifier
if ((0, tokens_1.isGroup)(identifier)) {
throw new ParserError.ExpectedFormError(this.source, identifier.location.start, identifier, "<identifier>");
}
if (identifier.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, identifier.pos, identifier, "<identifier>");
}
const convertedIdentifier = new scheme_node_types_1.Atomic.Identifier(this.toLocation(identifier), identifier.lexeme);
const convertedExpr = this.parseExpression(expr);
return new scheme_node_types_1.Atomic.Reassignment(group.location, convertedIdentifier, convertedExpr);
}
/**
* Parse a begin expression.
* @param group
* @returns
*/
parseBegin(group) {
// Form: (begin <body>+)
// ensure that the group has 2 or more elements
if (group.length() < 2) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(begin <body>+)");
}
const sequence = group.unwrap();
const sequenceElements = sequence.slice(1);
const convertedExpressions = [];
for (const sequenceElement of sequenceElements) {
convertedExpressions.push(this.parseExpression(sequenceElement));
}
return new scheme_node_types_1.Extended.Begin(group.location, convertedExpressions);
}
/**
* Parse a delay expression.
* @param group
* @returns
*/
parseDelay(group) {
// Form: (delay <expr>)
// ensure that the group has 2 elements
if (group.length() !== 2) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(delay <expr>)");
}
const elements = group.unwrap();
const expr = elements[1];
// Expr is treated as a single expression
const convertedExpr = this.parseExpression(expr);
return new scheme_node_types_1.Extended.Delay(group.location, convertedExpr);
}
// ___________________MISCELLANEOUS___________________
/**
* Parse an import expression.
* @param group
* @returns
*/
parseImport(group) {
// Form: (import "<source>" (<identifier>*))
// ensure that the group has 3 elements
if (group.length() !== 3) {
throw new ParserError.ExpectedFormError(this.source, group.firstToken().pos, group.firstToken(), '(import "<source>" (<identifier>*))');
}
const elements = group.unwrap();
const source = elements[1];
const identifiers = elements[2];
// source is treated as a single string
if (!(0, tokens_1.isToken)(source)) {
throw new ParserError.ExpectedFormError(this.source, source.location.start, source, '"<source>"');
}
if (source.type !== token_type_1.TokenType.STRING) {
throw new ParserError.ExpectedFormError(this.source, source.pos, source, '"<source>"');
}
// Identifiers are treated as a group of identifiers
if (!(0, tokens_1.isGroup)(identifiers)) {
throw new ParserError.ExpectedFormError(this.source, identifiers.pos, identifiers, "(<identifier>*)");
}
const identifierElements = identifiers.unwrap();
const convertedIdentifiers = [];
for (const identifierElement of identifierElements) {
if (!(0, tokens_1.isToken)(identifierElement)) {
throw new ParserError.ExpectedFormError(this.source, identifierElement.location.start, identifierElement, "<identifier>");
}
if (identifierElement.type !== token_type_1.TokenType.IDENTIFIER) {
throw new ParserError.ExpectedFormError(this.source, identifierElement.pos, identifierElement, "<identifier>");
}
convertedIdentifiers.push(new scheme_node_types_1.Atomic.Identifier(this.toLocation(identifierElement), identifierElement.lexeme));
}
const convertedSource = new scheme_node_types_1.Atomic.StringLiteral(this.toLocation(source), source.literal);
return new scheme_node_types_1.Atomic.Import(group.location, convertedSource, convertedIdentifiers);
}
/**
* Parse an export expression.
* @param group
* @returns
*/
parseExport(group) {
// Form: (export (<definition>))
// ensure that the group has 2 elements
if (group.length() !== 2) {
throw new ParserError.ExpectedFormError(this.source, group.firstToken().pos, group.firstToken(), "(export (<definition>))");
}
const elements = group.unwrap();
const definition = elements[1];
// assert that definition is a group
if (!(0, tokens_1.isGroup)(definition)) {
throw new ParserError.ExpectedFormError(this.source, definition.pos, definition, "(<definition>)");
}
const convertedDefinition = this.parseExpression(definition);
// assert that convertedDefinition is a definition
if (!(convertedDefinition instanceof scheme_node_types_1.Atomic.Definition ||
convertedDefinition instanceof scheme_node_types_1.Extended.FunctionDefinition)) {
throw new ParserError.ExpectedFormError(this.source, definition.location.start, definition, "(<definition>)");
}
return new scheme_node_types_1.Atomic.Export(group.location, convertedDefinition);
}
/**
* Parses a vector expression
*/
parseVector(group) {
// Because of the group invariants, we can safely assume that the group
// is strictly of size 2.
// Additionally, we can safely assume that the second element is a group
// because token HASH_VECTOR expects a parenthesis as the next immediate
// token.
const elements = group.unwrap()[1];
// Vectors will be treated normally regardless of the quote mode.
// but interior expressions will be affected by the mode.
const convertedElements = elements
.unwrap()
.map(this.parseExpression.bind(this));
return new scheme_node_types_1.Atomic.Vector(group.location, convertedElements);
}
// ___________________________________________________
/** Parses a sequence of tokens into an AST.
*
* @param group A group of tokens.
* @returns An AST.
*/
parse() {
// collect all top-level elements
const topElements = [];
while (!this.isAtEnd()) {
if (this.peek().type === token_type_1.TokenType.EOF) {
break;
}
const currentElement = this.grouping();
if (!currentElement) {
continue;
}
const convertedElement = this.parseExpression(currentElement);
topElements.push(convertedElement);
}
return topElements;
}
}
exports.SchemeParser = SchemeParser;
//# sourceMappingURL=scheme-parser.js.map