Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/zenscript/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ZenScriptMemberProvider } from './reference/member-provider'
import { ZenScriptNameProvider } from './reference/name-provider'
import { ZenScriptScopeComputation } from './reference/scope-computation'
import { ZenScriptScopeProvider } from './reference/scope-provider'
import { ZenScriptOverloadResolver } from './typing/overload-resolver'
import { ZenScriptTypeComputer } from './typing/type-computer'
import { ZenScriptTypeFeatures } from './typing/type-features'
import { registerValidationChecks, ZenScriptValidator } from './validation/validator'
Expand All @@ -38,6 +39,7 @@ export interface ZenScriptAddedServices {
typing: {
TypeComputer: ZenScriptTypeComputer
TypeFeatures: ZenScriptTypeFeatures
OverloadResolver: ZenScriptOverloadResolver
}
workspace: {
PackageManager: ZenScriptPackageManager
Expand Down Expand Up @@ -90,6 +92,7 @@ export const ZenScriptModule: Module<ZenScriptServices, PartialLangiumServices &
typing: {
TypeComputer: services => new ZenScriptTypeComputer(services),
TypeFeatures: services => new ZenScriptTypeFeatures(services),
OverloadResolver: services => new ZenScriptOverloadResolver(services),
},
lsp: {
CompletionProvider: services => new ZenScriptCompletionProvider(services),
Expand Down
19 changes: 17 additions & 2 deletions packages/zenscript/src/reference/member-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ZenScriptServices } from '../module'
import type { TypeComputer } from '../typing/type-computer'
import type { ZenScriptSyntheticAstType } from './synthetic'
import { EMPTY_STREAM, stream } from 'langium'
import { isClassDeclaration, isOperatorFunctionDeclaration, isVariableDeclaration } from '../generated/ast'
import { isClassDeclaration, isOperatorFunctionDeclaration, isScript, isVariableDeclaration } from '../generated/ast'
import { ClassType, isAnyType, isClassType, isFunctionType, type Type, type ZenScriptType } from '../typing/type-description'
import { isStatic, streamClassChain, streamDeclaredMembers } from '../utils/ast'
import { defineRules } from '../utils/rule'
Expand Down Expand Up @@ -80,7 +80,7 @@ export class ZenScriptMemberProvider implements MemberProvider {
return EMPTY_STREAM
}

if (isSyntheticAstNode(target)) {
if (isSyntheticAstNode(target) || isScript(target)) {
return this.streamMembers(target)
}

Expand All @@ -96,6 +96,21 @@ export class ZenScriptMemberProvider implements MemberProvider {
return this.streamMembers(type)
},

ParenthesizedExpression: (source) => {
const type = this.typeComputer.inferType(source)
return this.streamMembers(type)
},

PrefixExpression: (source) => {
const type = this.typeComputer.inferType(source)
return this.streamMembers(type)
},

InfixExpression: (source) => {
const type = this.typeComputer.inferType(source)
return this.streamMembers(type)
},

IndexingExpression: (source) => {
const type = this.typeComputer.inferType(source)
return this.streamMembers(type)
Expand Down
4 changes: 2 additions & 2 deletions packages/zenscript/src/reference/name-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ export class ZenScriptNameProvider extends DefaultNameProvider {
Script: source => source.$document ? getName(source.$document) : undefined,
ImportDeclaration: source => source.alias || source.path.at(-1)?.$refText,
FunctionDeclaration: source => source.name || 'lambda function',
ConstructorDeclaration: _ => 'zenConstructor',
ConstructorDeclaration: source => source.$container.name,
OperatorFunctionDeclaration: source => source.op,
})

private readonly nameNodeRules = defineRules<NameNodeRuleMap>({
ImportDeclaration: source => GrammarUtils.findNodeForProperty(source.$cstNode, 'alias'),
ConstructorDeclaration: source => GrammarUtils.findNodeForProperty(source.$cstNode, 'zenConstructor'),
ConstructorDeclaration: source => GrammarUtils.findNodeForKeyword(source.$cstNode, 'zenConstructor'),
OperatorFunctionDeclaration: source => GrammarUtils.findNodeForProperty(source.$cstNode, 'op'),
})
}
Expand Down
67 changes: 59 additions & 8 deletions packages/zenscript/src/reference/scope-provider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { AstNode, AstNodeDescription, ReferenceInfo, Scope, ScopeOptions, Stream } from 'langium'
import type { AstNode, AstNodeDescription, ReferenceInfo, Scope, ScopeOptions } from 'langium'
import type { ZenScriptAstType } from '../generated/ast'
import type { ZenScriptServices } from '../module'
import type { OverloadResolver } from '../typing/overload-resolver'
import type { ZenScriptDescriptionIndex } from '../workspace/description-index'
import type { PackageManager } from '../workspace/package-manager'
import type { DynamicProvider } from './dynamic-provider'
import type { MemberProvider } from './member-provider'
import { substringBeforeLast } from '@intellizen/shared'
import { AstUtils, DefaultScopeProvider, EMPTY_SCOPE, stream, StreamScope } from 'langium'
import { ClassDeclaration, ImportDeclaration, isClassDeclaration, TypeParameter } from '../generated/ast'
import { ClassDeclaration, ImportDeclaration, isCallExpression, isClassDeclaration, isConstructorDeclaration, isScript, TypeParameter } from '../generated/ast'
import { getPathAsString } from '../utils/ast'
import { defineRules } from '../utils/rule'
import { generateStream } from '../utils/stream'
Expand All @@ -20,13 +21,15 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider {
private readonly memberProvider: MemberProvider
private readonly dynamicProvider: DynamicProvider
private readonly descriptionIndex: ZenScriptDescriptionIndex
private readonly overloadResolver: OverloadResolver

constructor(services: ZenScriptServices) {
super(services)
this.packageManager = services.workspace.PackageManager
this.memberProvider = services.references.MemberProvider
this.dynamicProvider = services.references.DynamicProvider
this.descriptionIndex = services.workspace.DescriptionIndex
this.overloadResolver = services.typing.OverloadResolver
}

override getScope(context: ReferenceInfo): Scope {
Expand Down Expand Up @@ -68,8 +71,34 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider {
return this.createScopeForNodes(classes, outside)
}

override createScopeForNodes(nodes: Stream<AstNode>, outerScope?: Scope, options?: ScopeOptions): Scope {
return new StreamScope(nodes.map(it => this.descriptionIndex.getDescription(it)), outerScope, options)
private importedScope(source: ReferenceInfo, outside?: Scope) {
const script = AstUtils.findRootNode(source.container)
if (!isScript(script)) {
return EMPTY_SCOPE
}

const refText = source.reference.$refText
const imports = stream(script.imports)
.flatMap(it => this.descriptionIndex.createImportedDescriptions(it))

if (refText === '' || !isCallExpression(source.container.$container) || source.container.$containerProperty !== 'receiver') {
return this.createScope(imports, outside)
}

// TODO: Workaround for function overloading, may rework after langium supports multi-target references
const maybeCandidates = imports
.filter(it => it.name === refText)
.map(it => it.node)
.nonNullable()
.toArray()

const overloads = this.overloadResolver.resolveOverloads(source.container.$container, maybeCandidates)
const descriptions = overloads.map(it => this.descriptionIndex.createDynamicDescription(it, refText))
return this.createScope(descriptions, outside)
}

override createScopeForNodes(nodes: Iterable<AstNode>, outerScope?: Scope, options?: ScopeOptions): Scope {
return new StreamScope(stream(nodes).map(it => this.descriptionIndex.getDescription(it)), outerScope, options)
}

private readonly rules = defineRules<RuleMap>({
Expand Down Expand Up @@ -97,14 +126,27 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider {
let outer: Scope
outer = this.packageScope()
outer = this.globalScope(outer)
outer = this.importedScope(source, outer)
outer = this.dynamicScope(source.container, outer)

const processor = (desc: AstNodeDescription) => {
switch (desc.type) {
case TypeParameter:
return
case ImportDeclaration: {
return this.descriptionIndex.createImportedDescription(desc.node as ImportDeclaration)
return
}
case ClassDeclaration: {
const classDecl = desc.node as ClassDeclaration
const callExpr = source.container.$container
if (isCallExpression(callExpr) && source.container.$containerProperty === 'receiver') {
const constructors = classDecl.members.filter(isConstructorDeclaration)
const overloads = this.overloadResolver.resolveOverloads(callExpr, constructors)
if (overloads[0]) {
return this.descriptionIndex.getDescription(overloads[0])
}
}
return desc
}
default:
return desc
Expand All @@ -116,19 +158,28 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider {
MemberAccess: (source) => {
const outer = this.dynamicScope(source.container)
const members = this.memberProvider.streamMembers(source.container.receiver)
return this.createScopeForNodes(members, outer)

if (source.reference.$refText && isCallExpression(source.container.$container) && source.container.$containerProperty === 'receiver') {
const maybeCandidates = members.filter(it => this.nameProvider.getName(it) === source.reference.$refText).toArray()
const overloads = this.overloadResolver.resolveOverloads(source.container.$container, maybeCandidates)
return this.createScopeForNodes(overloads, outer)
}
else {
return this.createScopeForNodes(members, outer)
}
},

NamedTypeReference: (source) => {
if (!source.index) {
const outer = this.classScope()
let outer = this.packageScope()
outer = this.classScope(outer)
const processor = (desc: AstNodeDescription) => {
switch (desc.type) {
case TypeParameter:
case ClassDeclaration:
return desc
case ImportDeclaration: {
return this.descriptionIndex.createImportedDescription(desc.node as ImportDeclaration)
return this.descriptionIndex.createImportedDescriptions(desc.node as ImportDeclaration)[0]
}
}
}
Expand Down
183 changes: 183 additions & 0 deletions packages/zenscript/src/typing/overload-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import type { CallableDeclaration, CallExpression, Expression, ValueParameter } from '../generated/ast'
import type { ZenScriptServices } from '../module'
import type { TypeComputer } from './type-computer'
import type { TypeFeatures } from './type-features'
import { type AstNode, MultiMap } from 'langium'
import { isClassDeclaration, isConstructorDeclaration, isFunctionDeclaration } from '../generated/ast'

export interface OverloadResolver {
resolveOverloads: (callExpr: CallExpression, maybeCandidates: AstNode[]) => AstNode[]
}

export enum OverloadMatch {
ExactMatch,
VarargMatch,
OptionalArgMatch,
SubtypeMatch,
ImplicitCastMatch,
NotMatch,
}

function worstMatch(matchSet: Set<OverloadMatch>): OverloadMatch {
return Array.from(matchSet).sort((a, b) => a - b).at(-1) ?? OverloadMatch.NotMatch
}

export class ZenScriptOverloadResolver implements OverloadResolver {
private readonly typeComputer: TypeComputer
private readonly typeFeatures: TypeFeatures

constructor(services: ZenScriptServices) {
this.typeComputer = services.typing.TypeComputer
this.typeFeatures = services.typing.TypeFeatures
}

public resolveOverloads(callExpr: CallExpression, maybeCandidates: AstNode[]): AstNode[] {
if (!maybeCandidates.length) {
return []
}

let candidates: CallableDeclaration[]
if (maybeCandidates.find(isClassDeclaration)) {
candidates = maybeCandidates.find(isClassDeclaration)!.members.filter(isConstructorDeclaration)
}
else if (maybeCandidates.find(isFunctionDeclaration)) {
candidates = maybeCandidates.filter(isFunctionDeclaration)
}
else if (maybeCandidates.find(isConstructorDeclaration)) {
candidates = maybeCandidates.filter(isConstructorDeclaration)
}
else {
console.error(`Invalid overload candidates for call expression: ${callExpr.$cstNode?.text}`)
return []
}

if (candidates.length === 1) {
return candidates
}

const groupedCandidates = candidates.reduce<MultiMap<AstNode, CallableDeclaration>>((map, it) => map.add(it.$container, it), new MultiMap())
for (const container of groupedCandidates.keys()) {
const overloads = this.analyzeOverloads(new Set(groupedCandidates.get(container)), callExpr.arguments)
if (overloads.length) {
return overloads
}
else {
// FIXME: overloading error
// For debugging, consider adding a breakpoint here
console.error(`Could not resolve overloads for call expression: ${callExpr.$cstNode?.text}`)
}
}

return candidates
}

private analyzeOverloads(candidates: Set<CallableDeclaration>, args: Expression[]): CallableDeclaration[] {
const possibles = candidates.values()
.map(it => ({ candidate: it, match: this.matchSignature(it, args) }))
.filter(it => it.match !== OverloadMatch.NotMatch)
.toArray()
.sort((a, b) => a.match - b.match)
const groupedPossibles = Object.groupBy(possibles, it => it.match)
const bestMatches = Object.values(groupedPossibles).at(0) ?? []

if (bestMatches.length > 1) {
this.logAmbiguousOverloads(possibles, args)
}

return bestMatches.map(it => it.candidate)
}

private logAmbiguousOverloads(possibles: { candidate: CallableDeclaration, match: OverloadMatch }[], args: Expression[]) {
const first = possibles[0].candidate
const name = isConstructorDeclaration(first) ? first.$container.name : first.name
const types = args.map(it => this.typeComputer.inferType(it)?.toString()).join(', ')
console.warn(`ambiguous overload for ${name}(${types})`)
for (const { candidate, match } of possibles) {
const params = candidate.parameters
.map((it) => {
const str = this.typeComputer.inferType(it)?.toString() ?? 'undefined'
if (it.varargs) {
return `...${str}`
}
else if (it.defaultValue) {
return `${str}?`
}
else {
return str
}
}).join(', ')
console.warn(`----- ${OverloadMatch[match]} ${name}(${params})`)
}
}

private createParamToArgsMap(params: ValueParameter[], args: Expression[]): MultiMap<ValueParameter, Expression> {
const map = new MultiMap<ValueParameter, Expression>()
for (let a = 0, p = 0, arg = args[a], param = params[p]; a < args.length && p < params.length;) {
if (arg) {
map.add(param, arg)
arg = args[++a]
}
if (!param.varargs) {
param = params[++p]
}
}
return map
}

private matchSignature(callable: CallableDeclaration, args: Expression[]): OverloadMatch {
const params = [...callable.parameters]
const map = this.createParamToArgsMap(params, args)

const matchSet = new Set([OverloadMatch.ExactMatch])
if (args.length > map.size) {
matchSet.add(OverloadMatch.NotMatch)
}
else {
for (const param of params) {
const arg = map.get(param).at(0)
// special checking
if (param.varargs) {
matchSet.add(OverloadMatch.VarargMatch)
if (!arg) {
continue
}
}
else if (param.defaultValue) {
matchSet.add(OverloadMatch.OptionalArgMatch)
if (!arg) {
continue
}
}
else {
if (!arg) {
matchSet.add(OverloadMatch.NotMatch)
break
}
}

// type checking
const paramType = this.typeComputer.inferType(param)
const argType = this.typeComputer.inferType(arg)
if (!paramType || !argType) {
matchSet.add(OverloadMatch.ImplicitCastMatch)
continue
}

if (this.typeFeatures.areTypesEqual(paramType, argType)) {
matchSet.add(OverloadMatch.ExactMatch)
}
else if (this.typeFeatures.isSubType(argType, paramType)) {
matchSet.add(OverloadMatch.SubtypeMatch)
}
else if (this.typeFeatures.isConvertible(argType, paramType)) {
matchSet.add(OverloadMatch.ImplicitCastMatch)
}
else {
matchSet.add(OverloadMatch.NotMatch)
break
}
}
}
return worstMatch(matchSet)
}
}
Loading