@sylphx/synth-typecheck
Version:
Type checker for Synth AST - cross-language type inference and validation
464 lines (379 loc) • 11.2 kB
text/typescript
/**
* Type checker implementation
*/
import type { BaseNode, NodeId, Tree } from '@sylphx/synth'
import type { Type, TypeCheckResult, TypeEnvironment, TypeError } from './types.js'
/**
* Type checker
*/
export class TypeChecker {
private errors: TypeError[] = []
private types: Map<NodeId, Type> = new Map()
private env: TypeEnvironment = {
variables: new Map(),
functions: new Map(),
classes: new Map(),
}
/**
* Check types in a tree
*/
check(tree: Tree): TypeCheckResult {
this.errors = []
this.types = new Map()
this.env = this.createGlobalEnvironment()
// Traverse and infer types
this.inferTypes(tree, tree.root)
return {
success: this.errors.length === 0,
errors: this.errors,
types: this.types,
}
}
/**
* Get inferred type for a node
*/
getType(nodeId: NodeId): Type | undefined {
return this.types.get(nodeId)
}
/**
* Create global type environment with built-in types
*/
private createGlobalEnvironment(): TypeEnvironment {
const env: TypeEnvironment = {
variables: new Map(),
functions: new Map(),
classes: new Map(),
}
// Built-in types
env.variables.set('undefined', { kind: 'undefined' })
env.variables.set('null', { kind: 'null' })
// Built-in functions (common across languages)
env.functions.set('console.log', {
kind: 'function',
parameterTypes: [{ kind: 'any' }],
returnType: { kind: 'void' },
})
return env
}
/**
* Infer types for a node
*/
private inferTypes(tree: Tree, nodeId: NodeId): Type {
const node = tree.nodes[nodeId]
if (!node) {
return { kind: 'unknown' }
}
// Check if already inferred
const existing = this.types.get(nodeId)
if (existing) {
return existing
}
// Infer based on node type
let type: Type
switch (node.type) {
// Literals
case 'NumericLiteral':
case 'NumberLiteral':
type = { kind: 'number', value: node.data?.value }
break
case 'StringLiteral':
type = { kind: 'string', value: node.data?.value }
break
case 'BooleanLiteral':
type = { kind: 'boolean', value: node.data?.value }
break
case 'NullLiteral':
type = { kind: 'null' }
break
// Identifiers
case 'Identifier':
type = this.inferIdentifier(node)
break
// Variable declarations
case 'VariableDeclarator':
type = this.inferVariableDeclarator(tree, node)
break
// Function declarations
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'ArrowFunctionExpression':
type = this.inferFunction(tree, node)
break
// Binary operations
case 'BinaryExpression':
type = this.inferBinaryExpression(tree, node)
break
// Unary operations
case 'UnaryExpression':
type = this.inferUnaryExpression(tree, node)
break
// Logical operations
case 'LogicalExpression':
type = this.inferLogicalExpression(tree, node)
break
// Call expressions
case 'CallExpression':
type = this.inferCallExpression(tree, node)
break
// Array expressions
case 'ArrayExpression':
type = this.inferArrayExpression(tree, node)
break
// Object expressions
case 'ObjectExpression':
type = this.inferObjectExpression(tree, node)
break
// Member expressions
case 'MemberExpression':
type = this.inferMemberExpression(tree, node)
break
// Assignment expressions
case 'AssignmentExpression':
type = this.inferAssignmentExpression(tree, node)
break
default:
type = { kind: 'unknown' }
}
this.types.set(nodeId, type)
// Recurse through children
for (const childId of node.children) {
this.inferTypes(tree, childId)
}
return type
}
/**
* Infer type for identifier
*/
private inferIdentifier(node: BaseNode): Type {
const name = node.data?.name
if (!name) {
return { kind: 'unknown' }
}
// Look up in environment
const varType = this.env.variables.get(String(name))
if (varType) {
return varType
}
const fnType = this.env.functions.get(String(name))
if (fnType) {
return fnType
}
return { kind: 'unknown' }
}
/**
* Infer type for variable declarator
*/
private inferVariableDeclarator(tree: Tree, node: BaseNode): Type {
const id = node.data?.id as { name?: string } | undefined
const name = id?.name
const initId = node.children.find((childId) => {
const child = tree.nodes[childId]
return child && child.type !== 'Identifier'
})
let type: Type = { kind: 'unknown' }
if (initId) {
// Infer from initializer
type = this.inferTypes(tree, initId)
}
// Store in environment
if (name) {
this.env.variables.set(String(name), type)
}
return type
}
/**
* Infer type for function
*/
private inferFunction(_tree: Tree, node: BaseNode): Type {
const id = node.data?.id as { name?: string } | undefined
const name = node.data?.name || id?.name
const params = (node.data?.params || []) as unknown[]
const parameterTypes: Type[] = params.map(() => ({ kind: 'any' as const }))
const returnType: Type = { kind: 'any' } // Would need return statement analysis
const fnType: Type = {
kind: 'function',
parameterTypes,
returnType,
}
if (name) {
this.env.functions.set(String(name), fnType)
}
return fnType
}
/**
* Infer type for binary expression
*/
private inferBinaryExpression(tree: Tree, node: BaseNode): Type {
const operator = node.data?.operator
// Get operand types
const [leftId, rightId] = node.children
const leftType = leftId ? this.inferTypes(tree, leftId) : { kind: 'unknown' }
const rightType = rightId ? this.inferTypes(tree, rightId) : { kind: 'unknown' }
// Arithmetic operators
if (['+', '-', '*', '/', '%', '**'].includes(String(operator))) {
// String concatenation
if (operator === '+' && (leftType.kind === 'string' || rightType.kind === 'string')) {
return { kind: 'string' }
}
return { kind: 'number' }
}
// Comparison operators
if (['<', '>', '<=', '>=', '==', '===', '!=', '!=='].includes(String(operator))) {
return { kind: 'boolean' }
}
return { kind: 'unknown' }
}
/**
* Infer type for unary expression
*/
private inferUnaryExpression(_tree: Tree, node: BaseNode): Type {
const operator = node.data?.operator
if (operator === '!') {
return { kind: 'boolean' }
}
if (['+', '-', '~'].includes(String(operator))) {
return { kind: 'number' }
}
if (operator === 'typeof') {
return { kind: 'string' }
}
return { kind: 'unknown' }
}
/**
* Infer type for logical expression
*/
private inferLogicalExpression(_tree: Tree, node: BaseNode): Type {
const operator = node.data?.operator
if (operator === '&&' || operator === '||') {
return { kind: 'boolean' }
}
return { kind: 'unknown' }
}
/**
* Infer type for call expression
*/
private inferCallExpression(tree: Tree, node: BaseNode): Type {
// Get callee
const calleeId = node.children[0]
if (!calleeId) {
return { kind: 'unknown' }
}
const calleeType = this.inferTypes(tree, calleeId)
if (calleeType.kind === 'function' && calleeType.returnType) {
return calleeType.returnType
}
return { kind: 'unknown' }
}
/**
* Infer type for array expression
*/
private inferArrayExpression(tree: Tree, node: BaseNode): Type {
if (node.children.length === 0) {
return {
kind: 'array',
elementType: { kind: 'unknown' },
}
}
// Infer element type from first element
const firstType = this.inferTypes(tree, node.children[0]!)
return {
kind: 'array',
elementType: firstType,
}
}
/**
* Infer type for object expression
*/
private inferObjectExpression(tree: Tree, node: BaseNode): Type {
const properties = new Map<string, Type>()
for (const childId of node.children) {
const child = tree.nodes[childId]
if (!child) continue
if (child.type === 'Property' || child.type === 'ObjectProperty') {
const keyData = child.data?.key as { name?: string } | string | undefined
const key = typeof keyData === 'object' ? keyData?.name : keyData
if (key) {
const valueId = child.children[0]
const valueType: Type = valueId ? this.inferTypes(tree, valueId) : { kind: 'unknown' }
properties.set(String(key), valueType)
}
}
}
return {
kind: 'object',
properties,
}
}
/**
* Infer type for member expression
*/
private inferMemberExpression(tree: Tree, node: BaseNode): Type {
const [objectId, _propertyId] = node.children
if (!objectId) {
return { kind: 'unknown' }
}
const objectType = this.inferTypes(tree, objectId)
if (objectType.kind === 'object' && objectType.properties) {
const propData = node.data?.property as { name?: string } | string | undefined
const property = typeof propData === 'object' ? propData?.name : propData
if (property) {
const propType = objectType.properties.get(String(property))
if (propType) {
return propType
}
}
}
const propName = (node.data?.property as { name?: string } | undefined)?.name
if (objectType.kind === 'array' && propName === 'length') {
return { kind: 'number' }
}
return { kind: 'unknown' }
}
/**
* Infer type for assignment expression
*/
private inferAssignmentExpression(tree: Tree, node: BaseNode): Type {
const [, rightId] = node.children
if (!rightId) {
return { kind: 'unknown' }
}
return this.inferTypes(tree, rightId)
}
/**
* Check if two types are compatible
*/
isCompatible(type1: Type, type2: Type): boolean {
// any is compatible with everything
if (type1.kind === 'any' || type2.kind === 'any') {
return true
}
// unknown is compatible with everything
if (type1.kind === 'unknown' || type2.kind === 'unknown') {
return true
}
// Same kind
if (type1.kind === type2.kind) {
// For arrays, check element types
if (type1.kind === 'array' && type2.kind === 'array') {
if (type1.elementType && type2.elementType) {
return this.isCompatible(type1.elementType, type2.elementType)
}
}
return true
}
return false
}
}
/**
* Create a new type checker
*/
export function createChecker(): TypeChecker {
return new TypeChecker()
}
/**
* Check types in a tree
*/
export function check(tree: Tree): TypeCheckResult {
const checker = new TypeChecker()
return checker.check(tree)
}