js-slang
Version:
Javascript-based implementations of Source, written in Typescript
1,008 lines (1,007 loc) • 61.4 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");
const constants_1 = require("../types/constants");
/**
* 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;
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:
case token_type_1.TokenType.DEFINE_SYNTAX:
case token_type_1.TokenType.SYNTAX_RULES: // Chapter 4
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, or when dealing with the macro chapter,
// any keyword is instead treated as a symbol
if (this.quoteMode !== QuoteMode.NONE ||
this.chapter >= constants_1.MACRO_CHAPTER) {
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, constants_1.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, constants_1.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, constants_1.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:
this.validateChapter(affector, constants_1.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]);
}
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, constants_1.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) {
if (this.chapter >= constants_1.MACRO_CHAPTER) {
// disable any verification for the empty group
// the CSET machine will verify its validity
return new scheme_node_types_1.Atomic.Nil(group.location);
}
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, constants_1.BASIC_CHAPTER);
return this.parseLambda(group);
case token_type_1.TokenType.DEFINE:
this.validateChapter(firstElement, constants_1.BASIC_CHAPTER);
return this.parseDefinition(group);
case token_type_1.TokenType.IF:
this.validateChapter(firstElement, constants_1.BASIC_CHAPTER);
return this.parseConditional(group);
case token_type_1.TokenType.LET:
this.validateChapter(firstElement, constants_1.BASIC_CHAPTER);
return this.parseLet(group);
case token_type_1.TokenType.COND:
this.validateChapter(firstElement, constants_1.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, constants_1.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, constants_1.MUTABLE_CHAPTER);
return this.parseBegin(group);
case token_type_1.TokenType.DELAY:
this.validateChapter(firstElement, constants_1.MUTABLE_CHAPTER);
return this.parseDelay(group);
case token_type_1.TokenType.SET:
this.validateChapter(firstElement, constants_1.MUTABLE_CHAPTER);
return this.parseSet(group);
// Scheme full (macros)
case token_type_1.TokenType.DEFINE_SYNTAX:
this.validateChapter(firstElement, constants_1.MACRO_CHAPTER);
return this.parseDefineSyntax(group);
case token_type_1.TokenType.SYNTAX_RULES:
// should not be called outside of define-syntax!
throw new ParserError.UnexpectedFormError(this.source, firstElement.pos, firstElement);
// Scm-slang misc
case token_type_1.TokenType.IMPORT:
this.validateChapter(firstElement, constants_1.BASIC_CHAPTER);
return this.parseImport(group);
case token_type_1.TokenType.EXPORT:
this.validateChapter(firstElement, constants_1.BASIC_CHAPTER);
return this.parseExport(group);
case token_type_1.TokenType.VECTOR:
this.validateChapter(firstElement, constants_1.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) {
if (this.chapter >= constants_1.MACRO_CHAPTER) {
// disable any verification for the let expression
const groupItems = group.unwrap().slice(1);
groupItems.forEach(item => {
this.parseExpression(item);
});
return new scheme_node_types_1.Extended.Let(group.location, [], [], new scheme_node_types_1.Atomic.Identifier(group.location, "undefined"));
}
// 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) {
if (this.chapter >= constants_1.MACRO_CHAPTER) {
// disable any verification for the cond expression
const groupItems = group.unwrap().slice(1);
groupItems.forEach(item => {
this.parseExpression(item);
});
return new scheme_node_types_1.Extended.Cond(group.location, [], [], new scheme_node_types_1.Atomic.Identifier(group.location, "undefined"));
}
// 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) {
if (this.chapter >= constants_1.MACRO_CHAPTER) {
// disable any verification for the delay expression
const groupItems = group.unwrap().slice(1);
groupItems.forEach(item => {
this.parseExpression(item);
});
return new scheme_node_types_1.Extended.Delay(group.location, new scheme_node_types_1.Atomic.Identifier(group.location, "undefined"));
}
// 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);
}
// _____________________CHAPTER 3_____________________
/**
* Parse a define-syntax expression.
* @param group
* @returns nothing, this is for verification only.
*/
parseDefineSyntax(group) {
// Form: (define-syntax <identifier> <transformer>)
// ensure that the group has 3 elements
if (group.length() !== 3) {
throw new ParserError.ExpectedFormError(this.source, group.location.start, group, "(define-syntax <identifier> <transformer>)");
}
const elements = group.unwrap();
const identifier = elements[1];
const transformer = elements[2];
// parse the identifier using quote mode
// (to capture redefinitions of syntax)
this.quoteMode = QuoteMode.QUOTE;
const convertedIdentifier = this.parseExpression(identifier);
this.quoteMode = QuoteMode.NONE;
if (!(convertedIdentifier instanceof scheme_node_types_1.Atomic.Symbol)) {
throw new ParserError.ExpectedFormError(this.source, convertedIdentifier.location.start, identifier, "<identifier>");
}
// Transformer is treated as a group
// it should be syntax-rules
if (!(0, tokens_1.isGroup)(transformer)) {
throw new ParserError.ExpectedFormError(this.source, transformer.pos, transformer, "<transformer>");
}
if (transformer.length() < 2) {
throw new ParserError.ExpectedFormError(this.source, transformer.firstToken().pos, transformer, "(syntax-rules ...)");
}
const transformerToken = transformer.unwrap()[0];
if (!(0, tokens_1.isToken)(transformer.unwrap()[0])) {
throw new ParserError.ExpectedFormError(this.source, transformer.firstToken().pos, transformerToken, "syntax-rules");
}
if (transformerToken.type !== token_type_1.TokenType.SYNTAX_RULES) {
throw new ParserError.ExpectedFormError(this.source, transformerToken.pos, transformerToken, "syntax-rules");
}
// parse the transformer
const convertedTransformer = this.parseSyntaxRules(transformer);
return new scheme_node_types_1.Atomic.DefineSyntax(group.location, convertedIdentifier, convertedTransformer);
}
/**
* Helper function to verify the validity of a pattern.
* @param pattern
* @returns validity of the pattern
*/
isValidPattern(pattern) {
// a pattern is either a symbol, a literal or
// a list (<pattern>+), (<pattern>+ . <pattern>), (<pattern>+ ... <pattern>*)
// or (<pattern>+ ... <pattern>+ . <pattern>)
if (pattern instanceof scheme_node_types_1.Extended.List) {
// check if the list is a proper list
const isProper = pattern.terminator === undefined;
if (isProper) {
// scan to make sure that only one ellipsis is present
const ellipsisCount = pattern.elements.filter(item => item instanceof scheme_node_types_1.Atomic.Symbol && item.value === "...").length;
if (ellipsisCount > 1) {
return false;
}
const ellipsisIndex = pattern.elements.findIndex(item => item instanceof scheme_node_types_1.Atomic.Symbol && item.value === "...");
if (ellipsisIndex != -1) {
// check if the ellipsis is behind any other element
// (ie it's not the first element)
if (ellipsisIndex === 0) {
return false;
}
}
// recursively check the elements
for (const element of pattern.elements) {
if (!this.isValidPattern(element)) {
return false;
}
}
return true;
}
else {
// scan to make sure that only one ellipsis is present
const ellipsisCount = pattern.elements.filter(item => item instanceof scheme_node_types_1.Atomic.Symbol && item.value === "...").length;
if (ellipsisCount > 1) {
return false;
}
const ellipsisIndex = pattern.elements.findIndex(item => item instanceof scheme_node_types_1.Atomic.Symbol && item.value === "...");
if (ellipsisIndex != -1) {
// check if the ellipsis is behind any other element
// (ie it's not the first element)
if (ellipsisIndex === 0) {
return false;
}
// since this is an improper list, the ellipsis must not
// be the last element either
if (ellipsisIndex === pattern.elements.length - 1) {
return false;
}
}
// recursively check the elements
for (const element of pattern.elements) {
if (!this.isValidPattern(element)) {
return false;
}
}
return this.isValidPattern(pattern.terminator);
}
}
else if (pattern instanceof scheme_node_types_1.Atomic.Symbol ||
pattern instanceof scheme_node_types_1.Atomic.BooleanLiteral ||
pattern instanceof scheme_node_types_1.Atomic.NumericLiteral ||
pattern instanceof scheme_node_types_1.Atomic.StringLiteral ||
pattern instanceof scheme_node_types_1.Atomic.Nil) {
return true;
}
else {
return false;
}
}
/**
* Helper function to verify the validity of a template.
* @param template
* @returns validity of the template
*/
isValidTemplate(template) {
// a template is either a symbol, a literal or
// a list (<element>+), (<element>+ . <template>), (... <template>)
// where <element> is a template optionally followed by ...
if (template instanceof scheme_node_types_1.Extended.List) {
// check if the list is a proper list
const isProper = template.terminator === undefined;
if (isProper) {
// should have at least 1 element
if (template.elements.length === 0) {
return false;
}
// (... <template>) case
if (template.elements.length === 2 &&
template.elements[0] instanceof scheme_node_types_1.Atomic.Symbol &&
template.elements[0].value === "...") {
return this.isValidTemplate(template.elements[1]);
}
let ellipsisWorksOnLastElement = false;
// check each element for validity except for ellipses.
// for those, check if they follow a valid template.
for (let i = 0; i < template.elements.length; i++) {
const element = template.elements[i];
if (element instanceof scheme_node_types_1.Atomic.Symbol && element.value === "...") {
if (ellipsisWorksOnLastElement) {
ellipsisWorksOnLastElement = false;
continue;
}
// either consecutive ellipses or the first element is an ellipsis
return false;
}
else {
if (!this.isValidTemplate(element)) {
return false;