@@ -13,6 +13,7 @@ import {
1313 readLookupExpression ,
1414 type Expression ,
1515 type ExpressionContext ,
16+ type KeyPath ,
1617 type KeywordHandler ,
1718 type SemanticGraph ,
1819} from '../../../semantics.js'
@@ -42,6 +43,9 @@ export const lookupKeywordHandler: KeywordHandler = (
4243 }
4344 } )
4445
46+ /**
47+ * Recursively search upwards in lexical scope for the given `key`.
48+ */
4549const lookup = ( {
4650 context,
4751 key,
@@ -59,47 +63,100 @@ const lookup = ({
5963 } )
6064 : either . makeRight ( option . makeSome ( asSemanticGraph ( valueFromPrelude ) ) )
6165 } else {
66+ // Given the following program:
67+ // ```
68+ // {
69+ // a1: …
70+ // a2: {
71+ // b1: …
72+ // b2: … // we are here
73+ // }
74+ // }
75+ // ```
76+ // If `context.location` is `['a2', 'b2']`, the current scope (containing `b1`) is at `['a2']`,
77+ // and the parent scope (containing `a1`) is at `[]`.
6278 const pathToCurrentScope = context . location . slice ( 0 , - 1 )
79+ const pathToParentScope = pathToCurrentScope . slice ( 0 , - 1 )
6380
64- // TODO: This is sketchy, or at least confusingly-written. Improve test coverage to weed out
65- // potential bugginess, and consider refactoring to make it easier to follow.
66- const pathToPossibleExpression =
67- pathToCurrentScope [ pathToCurrentScope . length - 1 ] === '1'
68- ? pathToCurrentScope . slice ( 0 , - 1 )
69- : pathToCurrentScope
81+ // If parent is a keyword expression and the current scope's key is `1`, the current scope is
82+ // an expression argument.
83+ const expressionCurrentScopeIsArgumentOf = option . flatMap (
84+ option . filter (
85+ applyKeyPathToSemanticGraph ( context . program , pathToParentScope ) ,
86+ isExpression ,
87+ ) ,
88+ parent =>
89+ pathToCurrentScope [ pathToCurrentScope . length - 1 ] === '1'
90+ ? option . makeSome ( parent )
91+ : option . none ,
92+ )
7093
71- const possibleLookedUpValue = option . flatMap (
72- applyKeyPathToSemanticGraph ( context . program , pathToPossibleExpression ) ,
73- scope =>
74- either . match ( readFunctionExpression ( scope ) , {
75- left : _ =>
76- // Lookups should not resolve to expression properties.
77- // For example the value of the lookup expression in `a => :parameter` (desugared:
78- // `{@function , {parameter: a, body: {@lookup, {key: parameter} }}}`) should not be `a`.
79- isExpression ( scope )
80- ? option . none
81- : applyKeyPathToSemanticGraph ( scope , [ key ] ) ,
82- right : functionExpression =>
83- functionExpression [ 1 ] . parameter === key
84- ? // Keep an unelaborated `@lookup` around for resolution when the `@function` is called.
85- option . makeSome ( makeLookupExpression ( key ) )
86- : option . none ,
87- } ) ,
94+ type LookupResult =
95+ | {
96+ readonly kind : 'found'
97+ readonly foundValue : SemanticGraph
98+ }
99+ | {
100+ readonly kind : 'notFound'
101+ readonly nextLocationToCheckFrom : KeyPath
102+ }
103+
104+ const result : LookupResult = option . match (
105+ expressionCurrentScopeIsArgumentOf ,
106+ {
107+ some : parentExpression => {
108+ const parentFunctionResult = readFunctionExpression ( parentExpression )
109+ // If enclosed in a `@function` expression, allow looking up the parameter.
110+ if (
111+ either . isRight ( parentFunctionResult ) &&
112+ parentFunctionResult . value [ 1 ] . parameter === key
113+ ) {
114+ // Keep an unelaborated `@lookup` around for resolution when the `@function` is called.
115+ return {
116+ kind : 'found' ,
117+ foundValue : makeLookupExpression ( key ) ,
118+ }
119+ } else {
120+ return {
121+ kind : 'notFound' ,
122+ // Skip a level; don't consider expression properties as potential `@lookup` targets.
123+ nextLocationToCheckFrom : pathToParentScope ,
124+ }
125+ }
126+ } ,
127+ none : _ =>
128+ option . match (
129+ option . flatMap (
130+ applyKeyPathToSemanticGraph ( context . program , pathToCurrentScope ) ,
131+ currentScope => applyKeyPathToSemanticGraph ( currentScope , [ key ] ) ,
132+ ) ,
133+ {
134+ some : foundValue => ( {
135+ kind : 'found' ,
136+ foundValue,
137+ } ) ,
138+ none : _ => ( {
139+ kind : 'notFound' ,
140+ nextLocationToCheckFrom : pathToCurrentScope ,
141+ } ) ,
142+ } ,
143+ ) ,
144+ } ,
88145 )
89146
90- return option . match ( possibleLookedUpValue , {
91- none : ( ) =>
92- // Try the parent scope.
93- lookup ( {
94- key ,
95- context : {
96- keywordHandlers : context . keywordHandlers ,
97- location : pathToCurrentScope ,
98- program : context . program ,
99- } ,
100- } ) ,
101- some : lookedUpValue => either . makeRight ( option . makeSome ( lookedUpValue ) ) ,
102- } )
147+ if ( result . kind === 'found' ) {
148+ return either . makeRight ( option . makeSome ( result . foundValue ) )
149+ } else {
150+ // Try the parent scope.
151+ return lookup ( {
152+ key ,
153+ context : {
154+ keywordHandlers : context . keywordHandlers ,
155+ location : result . nextLocationToCheckFrom ,
156+ program : context . program ,
157+ } ,
158+ } )
159+ }
103160 }
104161}
105162
0 commit comments