Skip to content

Commit

Permalink
adding metalanguage with parser
Browse files Browse the repository at this point in the history
  • Loading branch information
maxdeliso committed Mar 8, 2025
1 parent 9a504ec commit a8227a8
Show file tree
Hide file tree
Showing 24 changed files with 560 additions and 250 deletions.
3 changes: 2 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export default tseslint.config(
'@typescript-eslint/no-unsafe-return': 'error',
'@stylistic/ts/semi': 'error',
'@stylistic/ts/indent': ['error', 2],
'@stylistic/ts/quotes': ['error', 'single']
'@stylistic/ts/quotes': ['error', 'single'],
'linebreak-style': ['error', 'unix']
},
}
);
2 changes: 1 addition & 1 deletion lib/consts/lambdas.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { parseLambda } from '../parser/untyped.js';

export const [, predLambda] = parseLambda('λn.λf.λx.n(λg.λh.h(gf))(λu.x)(λu.u)');
export const [, predLambda] = parseLambda('λn.λf.λx.n(λg.λh.h(g f))(λu.x)(λu.u)');
48 changes: 48 additions & 0 deletions lib/meta/trip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SKIExpression } from '../ski/expression.js';
import { UntypedLambda } from '../terms/lambda.js';
import { SystemFTerm } from '../terms/systemF.js';
import { TypedLambda } from '../types/typedLambda.js';
import { BaseType } from '../types/types.js';

export interface TripLangProgram {
kind: 'program';
terms: TripLangTerm[];
}

export type TripLangTerm =
| PolyDefinition
| TypedDefinition
| UntypedDefinition
| CombinatorDefinition
| TypeDefinition;

export interface PolyDefinition {
kind: 'poly';
name: string;
term: SystemFTerm;
}

export interface TypedDefinition {
kind: 'typed';
name: string;
type: BaseType;
term: TypedLambda;
}

export interface UntypedDefinition {
kind: 'untyped';
name: string;
term: UntypedLambda;
}

export interface CombinatorDefinition {
kind: 'combinator';
name: string;
term: SKIExpression;
}

export interface TypeDefinition {
kind: 'type';
name: string;
type: BaseType;
}
46 changes: 32 additions & 14 deletions lib/parser/chain.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { cons } from '../cons.js';
import { ParseError } from './parseError.js';
import { ParserState, remaining, peek } from './parserState.js';
import { ParserState, remaining, peek, skipWhitespace } from './parserState.js';

/**
* Parses a chain of expressions (applications) by repeatedly
* consuming atomic terms until either the input is exhausted or a
* termination token (')') is encountered.
* consuming atomic terms until either the input is exhausted,
* a termination token (')') is encountered, or a newline is found
* that's not part of whitespace within a term.
*
* @param state the current parser state.
* @param parseAtomic a function that parses an atomic term from the state,
Expand All @@ -17,23 +18,40 @@ export function parseChain<T>(
state: ParserState,
parseAtomic: (state: ParserState) => [string, T, ParserState]
): [string, T, ParserState] {
let resultStr = '';
const literals: string[] = [];
let resultTerm: T | undefined = undefined;
let currentState = state;
let currentState = skipWhitespace(state);

for (;;) {
// Check if any input remains.
const [hasRemaining, stateAfterRemaining] = remaining(currentState);
const [hasRemaining] = remaining(currentState);
if (!hasRemaining) break;

// Peek the next non-whitespace character.
const [peeked, stateAfterPeek] = peek(stateAfterRemaining);
// Terminate if we encounter a closing parenthesis.
const [peeked] = peek(currentState);

if (peeked === ')') break;

// Parse the next atomic term.
const [atomLit, atomTerm, newState] = parseAtomic(stateAfterPeek);
resultStr += atomLit;
// Check if we're at a newline that's not part of whitespace within a term
if (peeked === '\n' || peeked === '\r') {
// If we're at a newline, stop parsing this term
break;
}

const nextChars = currentState.buf.slice(currentState.idx, currentState.idx + 5);

// Terminate if we encounter a keyword
if (
nextChars === 'typed' ||
nextChars === 'type ' ||
nextChars === 'poly ' ||
nextChars === 'untyp' ||
nextChars === 'combi') {
break;
}

const [atomLit, atomTerm, newState] = parseAtomic(currentState);
literals.push(atomLit);

// If this is the first term, use it; otherwise, chain via cons.
if (resultTerm === undefined) {
Expand All @@ -42,13 +60,13 @@ export function parseChain<T>(
resultTerm = cons(resultTerm, atomTerm) as T;
}

// Update the state.
currentState = newState;
// Update the state and skip any whitespace.
currentState = skipWhitespace(newState);
}

if (resultTerm === undefined) {
throw new ParseError('expected a term');
}

return [resultStr, resultTerm, currentState];
return [literals.join(' '), resultTerm, currentState];
}
106 changes: 43 additions & 63 deletions lib/parser/parserState.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,75 @@
import { ParseError } from './parseError.js';

/**
* An immutable parser state.
*/
export interface ParserState {
buf: string;
idx: number;
}

/**
* Creates a new parser state from the given string.
*/
export function createParserState(buf: string): ParserState {
return { buf, idx: 0 };
}

/**
* Advances the index past any whitespace characters.
*/
function skipWhitespace(rdb: ParserState): ParserState {
let idx = rdb.idx;
while (idx < rdb.buf.length && /\s/.test(rdb.buf[idx])) {
export function skipWhitespace(state: ParserState): ParserState {
let idx = state.idx;
while (idx < state.buf.length && /\s/.test(state.buf[idx])) {
idx++;
}
return { buf: rdb.buf, idx };
return { buf: state.buf, idx };
}

/**
* Returns the next non‐whitespace character (or null if at end‐of-buffer),
* along with the updated state.
*/
export function peek(rdb: ParserState): [string | null, ParserState] {
const state = skipWhitespace(rdb);
if (state.idx < state.buf.length) {
return [state.buf[state.idx], state];
export function peek(state: ParserState): [string | null, ParserState] {
const newState = skipWhitespace(state);
if (newState.idx < newState.buf.length) {
return [newState.buf[newState.idx], newState];
}
return [null, state];
return [null, newState];
}

/**
* Consumes one character and returns the updated state.
*/
export function consume(rdb: ParserState): ParserState {
return { buf: rdb.buf, idx: rdb.idx + 1 };
export function consume(state: ParserState): ParserState {
return { buf: state.buf, idx: state.idx + 1 };
}

/**
* Matches the given character (after skipping whitespace). Throws a ParseError
* if the next non‐whitespace character is not the expected one.
*/
export function matchCh(rdb: ParserState, ch: string): ParserState {
const [next, state] = peek(rdb);
export function matchCh(state: ParserState, ch: string): ParserState {
const [next, newState] = peek(state);
if (next !== ch) {
throw new ParseError(`expected ${ch} but found ${next ?? 'null'}`);
throw new ParseError(`expected '${ch}' but found '${next ?? 'EOF'}'`);
}
return consume(state);
return consume(newState);
}

/**
* Matches a left parenthesis.
*/
export function matchLP(rdb: ParserState): ParserState {
return matchCh(rdb, '(');
export function matchLP(state: ParserState): ParserState {
return matchCh(state, '(');
}

/**
* Matches a right parenthesis.
*/
export function matchRP(rdb: ParserState): ParserState {
return matchCh(rdb, ')');
export function matchRP(state: ParserState): ParserState {
return matchCh(state, ')');
}

/**
* Checks whether there is any non‐whitespace character left, returning both the
* boolean result and the updated state.
*/
export function remaining(rdb: ParserState): [boolean, ParserState] {
const state = skipWhitespace(rdb);
return [state.idx < state.buf.length, state];
}

/**
* Parses a variable from the input. The variable is expected to be a letter.
*/
export function parseVariable(rdb: ParserState): [string, ParserState] {
const [next, state] = peek(rdb);
if (next === null) {
throw new ParseError('failed to parse variable: no next character');
export function parseIdentifier(state: ParserState): [string, ParserState] {
let id = '';
let currentState = skipWhitespace(state);
while (currentState.idx < currentState.buf.length) {
const ch = currentState.buf[currentState.idx];
if (!/[a-zA-Z0-9_]/.test(ch)) break;
id += ch;
currentState = consume(currentState);
}
if (id.length === 0) {
throw new ParseError('expected an identifier');
}
if (!/[a-zA-Z]/.test(next)) {
throw new ParseError(`failed to parse variable: ${next} did not match`);
return [id, currentState];
}

export function remaining(state: ParserState): [boolean, ParserState] {
const newState = skipWhitespace(state);
return [newState.idx < newState.buf.length, newState];
}

export function parseKeyword(state: ParserState, keywords: string[]): [string, ParserState] {
const [word, nextState] = parseIdentifier(state);
if (!keywords.includes(word)) {
throw new ParseError(`expected keyword, found ${word}`);
}
return [next, consume(state)];
return [word, nextState];
}
Loading

0 comments on commit a8227a8

Please sign in to comment.