cg/src/ast.ts

443 lines
12 KiB
TypeScript

type LiteralValue =
| { kind: 'int', value: number }
| { kind: 'float', value: number }
| { kind: 'string', value: string };
// Literals and Variables
export type Literal = {
kind: 'literal'
value: LiteralValue
line?: number
column?: number
start?: number
}
export type Variable = {
kind: 'variable'
name: string
line?: number
column?: number
start?: number
}
export type Constructor = {
kind: 'constructor'
name: string
line?: number
column?: number
start?: number
}
// Functions
export type Lambda = {
kind: 'lambda'
params: string[]
body: AST
line?: number
column?: number
start?: number
}
export type Apply = {
kind: 'apply'
func: AST
args: AST[]
line?: number
column?: number
start?: number
}
// Bindings
export type Let = {
kind: 'let'
name: string
value: AST
body: AST
line?: number
column?: number
start?: number
}
// Matching
export type Match = {
kind: 'match'
expr: AST
cases: MatchCase[]
line?: number
column?: number
start?: number
}
export type MatchCase = {
pattern: Pattern
result: AST
line?: number
column?: number
start?: number
}
export type Pattern =
| { kind: 'wildcard' }
| { kind: 'var', name: string }
| { kind: 'literal', value: number | string }
| { kind: 'constructor', name: string, args: Pattern[] }
| { kind: 'list', elements: Pattern[] }
| { kind: 'list-spread', head: Pattern[], spread: string }
| { kind: 'record', fields: { [key: string]: Pattern } }
// Data Structures
export type ListSpread = {
kind: 'list-spread'
spread: AST;
line?: number;
column?: number;
start?: number;
}
export type List = {
kind: 'list'
elements: (AST | ListSpread)[]
line?: number
column?: number
start?: number
}
export type Record = {
kind: 'record'
entries: Array<
| { kind: 'field', key: string, value: AST }
| { kind: 'spread', expr: AST }
>
// fields: { [key: string]: AST }
line?: number
column?: number
start?: number
}
export type RecordAccess = {
kind: 'record-access'
record: AST
field: string
line?: number
column?: number
start?: number
}
export type RecordUpdate = {
kind: 'record-update'
record: AST
updates: { [key: string]: AST }
line?: number
column?: number
start?: number
}
// Top-level constructs
export type Definition = {
kind: 'definition'
name: string
body?: AST
line?: number
column?: number
start?: number
annotation?: Annotation
module?: string
}
export type Rebind = {
kind: 'rebind'
target: AST
value: AST
line?: number
column?: number
start?: number
}
export type TypeAST =
| { kind: 'type-name', name: string } // Int, String, Bool
| { kind: 'type-var', name: string } // a, b
| { kind: 'type-function', param: TypeAST, result: TypeAST } // a \ b
| { kind: 'type-apply', constructor: TypeAST, args: TypeAST[] } // List a, Maybe Int
| { kind: 'type-record', fields: { name: string, type: TypeAST }[] } // { x: Int }
export type TypeConstructor = { name: string, args: TypeAST[] }
export type TypeDefinition = {
kind: 'type-definition'
name: string
params: string[]
constructors: TypeConstructor[]
line?: number
column?: number
start?: number
module?: string
}
export type Annotation = {
constraints: Constraint[]
type: TypeAST
}
export type Constraint = {
className: string
typeVar: string
}
export type ClassDefinition = {
kind: 'class-definition'
name: string
param: string
methods: { name: string, type: TypeAST }[]
line?: number
column?: number
start?: number
module?: string
}
export type InstanceDeclaration = {
kind: 'instance-declaration'
typeName: string
className: string
line?: number
column?: number
start?: number
module?: string
}
export type AST =
| Literal
| Variable
| Constructor
| Lambda
| Apply
| Let
| Match
| Record
| RecordAccess
| RecordUpdate
| List
| ListSpread
| Definition
| Rebind
const infixOps: { [key: string]: string } = {
cat: '&', add: '+', sub: '-', mul: '*', div: '/', mod: '%',
pow: '^', eq: '==', neq: '!=', gt: '>', lt: '<', gte: '>=', lte: '<='
};
const isInfix = (a: AST) => a.kind === 'apply' && a.func.kind === 'variable' && a.args.length === 2 && infixOps[a.func.name];
export function prettyPrint(ast: AST, indent = 0): string {
const i = ' '.repeat(indent);
switch (ast.kind) {
case 'literal': {
const val = ast.value;
if (val.kind === 'string') {
const escaped = val.value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
// .replace(/\n/g, '\\n')
.replace(/\t/g, '\\t');
return `"${escaped}"`;
}
return `${val.value}`;
}
case 'variable':
case 'constructor':
return ast.name;
case 'apply':
// infix ops
if (isInfix(ast)) {
const wrapIfNeeded = (a: AST) => {
const printed = prettyPrint(a, indent);
if (a.kind === 'apply' || a.kind === 'lambda' || a.kind === 'match' || a.kind === 'let') {
return `(${printed})`;
}
return `${printed}`;
}
const left = wrapIfNeeded(ast.args[0]);
const right = wrapIfNeeded(ast.args[1]);
return `${left} ${infixOps[(ast.func as Variable).name]} ${right}`;
}
const func = prettyPrint(ast.func, indent);
const args = ast.args.map(a => {
const printed = prettyPrint(a, indent);
if (a.kind === 'lambda' || a.kind === 'match' || a.kind === 'let' || a.kind === 'rebind' || a.kind === 'apply') {
return `(${printed})`;
}
return printed;
}).join(' ');
if (ast.func.kind === 'lambda' || ast.func.kind === 'match' || ast.func.kind === 'let') {
return `(${func} ${args})`
}
return `${func} ${args}`
case 'let':
const sep = indent === 0 ? '\n\n' : '\n';
return `${ast.name} = ${prettyPrint(ast.value, indent + 1)};${sep}${i}${prettyPrint(ast.body, indent)}`
case 'list': {
const elems = ast.elements.map(e => prettyPrint(e, indent + 1))
const oneLine = `[${elems.join(', ')}]`;
if (oneLine.length <= 60 || elems.length <= 1) return oneLine;
const inner = elems.map(e => `${' '.repeat(indent + 1)}${e}`).join(',\n');
return `[\n${inner}\n${i}]`;
}
case 'record':
const parts = ast.entries.map(entry =>
entry.kind === 'spread'
? `...${prettyPrint(entry.expr, indent + 1)}`
: `${needsQuotes(entry.key) ? JSON.stringify(entry.key) : entry.key} = ${prettyPrint(entry.value, indent + 1)}`
);
const oneLine = `{ ${parts.join(', ')} }`;
if (oneLine.length <= 60 || parts.length <= 1) return oneLine;
// if (parts.length <= 1) return `{ ${parts.join(', ')} }`;
const inner = parts.map(p => `${' '.repeat(indent + 1)}${p}`).join(',\n');
return `{\n${inner}\n${i}}`;
case 'lambda': {
const params = ast.params.join(' ');
const body = prettyPrint(ast.body, indent + 1);
const isComplex = ast.body.kind === 'match' || ast.body.kind === 'let';
if (isComplex) {
const ii = ' '.repeat(indent + 1);
return `${params} \\\n${ii}${body}`
}
return `${params} \\ ${body}`
}
case 'record-access':
const field = needsQuotes(ast.field) ? `${JSON.stringify(ast.field)}` : ast.field;
return `${prettyPrint(ast.record, indent)}.${field}`;
case 'record-update': {
const updates = Object.entries(ast.updates)
.map(([k, v]) => `${needsQuotes(k) ? `${JSON.stringify(k)}` : k} = ${prettyPrint(v, indent)}`)
.join(', ');
return `${prettyPrint(ast.record, indent)}.{ ${updates} }`
}
case 'match':
const expr = prettyPrint(ast.expr, indent);
const cases = ast.cases
.map(c => {
const result = prettyPrint(c.result, indent + 1);
const needsParens = c.result.kind === 'match' || c.result.kind === 'let';
return `${i}| ${prettyPrintPattern(c.pattern)} \\ ${needsParens ? '(' : ''}${result}${needsParens ? ')' : ''}`;
}).join('\n');
return `${expr}\n${cases}`;
case 'rebind':
return `${prettyPrint(ast.target, indent)} := ${prettyPrint(ast.value, indent)}`;
case 'list-spread':
return `...${prettyPrint(ast.spread, indent)}`;
case 'definition':
const displayName = /^_\d+$/.test(ast.name) ? '_' : ast.name;
const ann = ast.annotation
? ` : ${ast.annotation.constraints.length > 0
? ast.annotation.constraints.map(c => `${c.className} ${c.typeVar}`).join(', ') + ' :: '
: ''}${prettyPrintType(ast.annotation.type)}`
: '';
if (!ast.body) return `${displayName}${ann};`;
return `${displayName}${ann} = ${prettyPrint(ast.body, indent)};`;
default:
return `Unknown AST kind: ${i}${(ast as any).kind}`
}
}
function prettyPrintPattern(pattern: Pattern): string {
switch (pattern.kind) {
case 'wildcard':
return '_';
case 'var':
return pattern.name;
case 'literal':
return JSON.stringify(pattern.value);
case 'constructor':
if (pattern.args.length === 0) {
return pattern.name;
}
const args = pattern.args.map(prettyPrintPattern).join(' ');
return `(${pattern.name} ${args})`;
case 'list':
const elems = pattern.elements.map(prettyPrintPattern).join(', ');
return `[${elems}]`;
case 'list-spread':
const head = pattern.head.map(prettyPrintPattern).join(', ');
return head.length > 0
? `[${head}, ...${pattern.spread}]`
: `[...${pattern.spread}]`;
case 'record':
const fields = Object.entries(pattern.fields)
.map(([k, p]) => `${needsQuotes(k) ? `${JSON.stringify(k)}` : k} = ${prettyPrintPattern(p)}`)
.join(', ');
return `{${fields}}`;
default:
return `Unknown AST kind: ${(pattern as any).kind}`
}
}
export function prettyPrintType(type: TypeAST): string {
switch (type.kind) {
case 'type-name':
case 'type-var':
return type.name;
case 'type-function':
const param = type.param.kind === 'type-function'
? `(${prettyPrintType(type.param)})`
: prettyPrintType(type.param);
return `${param} \\ ${prettyPrintType(type.result)}`;
case 'type-apply':
const args = type.args.map(a =>
a.kind === 'type-function' || a.kind === 'type-apply'
? `(${prettyPrintType(a)})`
: prettyPrintType(a)
).join(' ');
return `${prettyPrintType(type.constructor)} ${args}`;
case 'type-record':
const fields = type.fields
.map(f => `${f.name} : ${prettyPrintType(f.type)}`)
.join(', ');
return `{ ${fields} }`;
}
}
// function prettyPrintTypeDefinition(td: TypeDefinition): string {
// const params = td.params.length > 0 ? ' ' + td.params.join(' ') : '';
// const ctors = td.constructors.map(c => {
// const args = c.args.map(a =>
// a.kind === 'type-function' || a.kind === 'type-apply'
// ? `(${prettyPrintType(a)})`
// : prettyPrintType(a)
// ).join(' ');
// return args ? `${c.name} ${args}` : c.name;
// }).join(' | ');
// return `${td.name}${params} = ${ctors};`;
// }
function needsQuotes(key: string): boolean {
return key === '_' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
}