Skip to content

Commit 0865101

Browse files
feat: remove quote requirement from repeat() macro
- Changed syntax from repeat('[C]', 3) to repeat([C], 3) - Parser now collects SELFIES tokens directly instead of string - Updated all tests and fixtures to use new syntax - Added repeat examples to README.md - Fixed analyzer getDependencies() to handle REPEAT_CALL tokens - All 625 tests passing 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6b93412 commit 0865101

6 files changed

Lines changed: 142 additions & 28 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ selfies list molecules.selfies
112112
# Reference other fragments
113113
[ethanol] = [ethyl][hydroxyl]
114114
115+
# Use repeat() macro for patterns
116+
[benzene] = repeat([C][=C], 3)[Ring1][=Branch1]
117+
[carbon_chain] = repeat([C], 10)
118+
[polymer] = repeat([monomer], 5)
119+
115120
# Import from other files
116121
import "./other-file.selfies" # import all
117122
import [methyl, ethyl] from "./fragments.selfies" # import specific
@@ -156,6 +161,33 @@ await initRDKit()
156161
const svg = await renderSelfies('[C][C][O]', { width: 300, height: 300 })
157162
```
158163

164+
## Repeat Macro
165+
166+
The `repeat()` macro allows you to repeat molecular patterns, perfect for polymers and long chains:
167+
168+
```selfies
169+
# Benzene ring
170+
[benzene] = repeat([C][=C], 3)[Ring1][=Branch1]
171+
172+
# Carbon chains
173+
[decane] = repeat([C], 10)
174+
[pentadecane] = repeat([C], 15)
175+
176+
# Polymer repeat units
177+
[PE_unit] = [C][C]
178+
[polyethylene_trimer] = repeat([PE_unit], 3)
179+
180+
# References work too
181+
[monomer] = [C][Branch1][C][Cl][C]
182+
[pvc_hexamer] = repeat([monomer], 6)
183+
```
184+
185+
The pattern can be:
186+
- **Primitive tokens**: `repeat([C], 10)` for a 10-carbon chain
187+
- **Complex patterns**: `repeat([C][=C], 3)` for alternating double bonds
188+
- **Named references**: `repeat([unit], 5)` to repeat a defined fragment
189+
- **Combined**: `[molecule] = [N]repeat([C], 3)[O]` with tokens before/after
190+
159191
## VS Code Extension
160192

161193
Get live visualization as you author `.selfies` files. See the molecular structure update line-by-line as you navigate your code.

src/dsl/analyzer.js

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,60 @@ export function getDependencies(program, name) {
2525

2626
const dependencies = []
2727
for (const token of definition.tokens) {
28-
const tokenName = token.slice(1, -1) // Remove brackets
29-
if (program.definitions.has(tokenName)) {
30-
if (!dependencies.includes(tokenName)) {
31-
dependencies.push(tokenName)
28+
if (typeof token === 'object' && token.type === 'REPEAT_CALL') {
29+
// Extract dependencies from repeat pattern
30+
const patternTokens = tokenizePattern(token.pattern)
31+
for (const patternToken of patternTokens) {
32+
const tokenName = patternToken.slice(1, -1) // Remove brackets
33+
if (program.definitions.has(tokenName)) {
34+
if (!dependencies.includes(tokenName)) {
35+
dependencies.push(tokenName)
36+
}
37+
}
38+
}
39+
} else if (typeof token === 'string') {
40+
const tokenName = token.slice(1, -1) // Remove brackets
41+
if (program.definitions.has(tokenName)) {
42+
if (!dependencies.includes(tokenName)) {
43+
dependencies.push(tokenName)
44+
}
3245
}
3346
}
3447
}
3548

3649
return dependencies
3750
}
3851

52+
/**
53+
* Tokenizes a pattern string into SELFIES tokens
54+
* @param {string} pattern - Pattern string like '[C][=C]'
55+
* @returns {string[]} Array of tokens
56+
*/
57+
function tokenizePattern(pattern) {
58+
const tokens = []
59+
let i = 0
60+
61+
while (i < pattern.length) {
62+
if (pattern[i] === '[') {
63+
// Find the closing bracket
64+
let j = i + 1
65+
while (j < pattern.length && pattern[j] !== ']') {
66+
j++
67+
}
68+
if (j < pattern.length) {
69+
tokens.push(pattern.slice(i, j + 1))
70+
i = j + 1
71+
} else {
72+
i++
73+
}
74+
} else {
75+
i++
76+
}
77+
}
78+
79+
return tokens
80+
}
81+
3982
/**
4083
* Gets the names that depend on a definition
4184
* @param {Object} program - Program object

src/dsl/parser.js

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,37 @@ function parseRepeatCall(tokens, startIndex) {
222222
}
223223
i++
224224

225-
// Expect STRING (pattern)
226-
if (i >= tokens.length || tokens[i].type !== TokenType.STRING) {
227-
// Skip to closing paren or end of line on error
225+
// Collect SELFIES_TOKENs as pattern until we hit COMMA
226+
const patternTokens = []
227+
const patternStart = i
228+
229+
while (i < tokens.length &&
230+
tokens[i].type !== TokenType.COMMA &&
231+
tokens[i].type !== TokenType.RPAREN &&
232+
tokens[i].type !== TokenType.NEWLINE &&
233+
tokens[i].type !== TokenType.EOF) {
234+
if (tokens[i].type === TokenType.SELFIES_TOKEN) {
235+
patternTokens.push(tokens[i].value)
236+
i++
237+
} else {
238+
const skipToEnd = skipToRParenOrEOL(tokens, i)
239+
return {
240+
error: createDiagnostic('Expected SELFIES tokens or name references in pattern', 'error', tokens[i]),
241+
nextIndex: skipToEnd
242+
}
243+
}
244+
}
245+
246+
if (patternTokens.length === 0) {
228247
const skipToEnd = skipToRParenOrEOL(tokens, i)
229248
return {
230-
error: createDiagnostic('Expected string pattern as first argument', 'error', tokens[i] || repeatToken),
249+
error: createDiagnostic('Pattern cannot be empty', 'error', tokens[patternStart] || repeatToken),
231250
nextIndex: skipToEnd
232251
}
233252
}
234-
const patternToken = tokens[i]
235-
const pattern = patternToken.value.slice(1, -1) // Remove quotes
236-
i++
253+
254+
// Join pattern tokens into a single string
255+
const pattern = patternTokens.join('')
237256

238257
// Expect COMMA
239258
if (i >= tokens.length || tokens[i].type !== TokenType.COMMA) {

src/dsl/resolver.test.js

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,81 +100,82 @@ describe('resolveAll', () => {
100100

101101
describe('repeat macro', () => {
102102
test('repeats a simple token sequence', () => {
103-
const program = parse('[triple_carbon] = repeat(\'[C]\', 3)')
103+
const program = parse('[triple_carbon] = repeat([C], 3)')
104104
expect(resolve(program, 'triple_carbon')).toBe('[C][C][C]')
105105
})
106106

107107
test('repeats a complex token sequence', () => {
108-
const program = parse('[benzene] = repeat(\'[C][=C]\', 3)[Ring1][=Branch1]')
108+
const program = parse('[benzene] = repeat([C][=C], 3)[Ring1][=Branch1]')
109109
expect(resolve(program, 'benzene')).toBe('[C][=C][C][=C][C][=C][Ring1][=Branch1]')
110110
})
111111

112112
test('repeats with count of 1', () => {
113-
const program = parse('[single] = repeat(\'[C][O]\', 1)')
113+
const program = parse('[single] = repeat([C][O], 1)')
114114
expect(resolve(program, 'single')).toBe('[C][O]')
115115
})
116116

117117
test('repeats with count of 0 produces empty sequence', () => {
118-
const program = parse('[empty] = [C]repeat(\'[O]\', 0)[C]')
118+
const program = parse('[empty] = [C]repeat([O], 0)[C]')
119119
expect(resolve(program, 'empty')).toBe('[C][C]')
120120
})
121121

122122
test('repeat with reference to other definition', () => {
123-
const source = '[unit] = [C][=C]\n[triple] = repeat(\'[unit]\', 3)'
123+
const source = '[unit] = [C][=C]\n[triple] = repeat([unit], 3)'
124124
const program = parse(source)
125125
expect(resolve(program, 'triple')).toBe('[C][=C][C][=C][C][=C]')
126126
})
127127

128128
test('multiple repeat calls in one definition', () => {
129-
const program = parse('[chain] = repeat(\'[C]\', 2)repeat(\'[O]\', 2)')
129+
const program = parse('[chain] = repeat([C], 2)repeat([O], 2)')
130130
expect(resolve(program, 'chain')).toBe('[C][C][O][O]')
131131
})
132132

133133
test('repeat combined with regular tokens', () => {
134-
const program = parse('[molecule] = [N]repeat(\'[C]\', 3)[O]')
134+
const program = parse('[molecule] = [N]repeat([C], 3)[O]')
135135
expect(resolve(program, 'molecule')).toBe('[N][C][C][C][O]')
136136
})
137137

138138
test('repeat with nested brackets in pattern', () => {
139-
const program = parse('[branched] = repeat(\'[C][Branch1][C][O]\', 2)')
139+
const program = parse('[branched] = repeat([C][Branch1][C][O], 2)')
140140
expect(resolve(program, 'branched')).toBe('[C][Branch1][C][O][C][Branch1][C][O]')
141141
})
142142

143143
test('throws error on invalid repeat count', () => {
144-
const program = parse('[bad] = repeat(\'[C]\', -1)')
144+
const program = parse('[bad] = repeat([C], -1)')
145145
expect(() => resolve(program, 'bad')).toThrow(/count must be/)
146146
})
147147

148148
test('throws error on non-numeric count', () => {
149-
const program = parse('[bad] = repeat(\'[C]\', abc)')
149+
const program = parse('[bad] = repeat([C], abc)')
150150
expect(() => resolve(program, 'bad')).toThrow()
151151
})
152152

153153
test('throws error on missing arguments', () => {
154-
const program = parse('[bad] = repeat(\'[C]\')')
154+
const program = parse('[bad] = repeat([C])')
155155
expect(() => resolve(program, 'bad')).toThrow()
156156
})
157157

158-
test('throws error on malformed repeat syntax', () => {
159-
const program = parse('[bad] = repeat([C], 3)')
160-
expect(() => resolve(program, 'bad')).toThrow()
158+
test('throws error on empty pattern', () => {
159+
const program = parse('[bad] = repeat(, 3)')
160+
// Parse error results in empty definition
161+
expect(() => resolve(program, 'bad')).toThrow(/no tokens/)
161162
})
162163

163164
test('simple polymer-like chain', () => {
164-
const source = '[ch2] = [C]\n[polymer_chain] = repeat(\'[ch2]\', 5)'
165+
const source = '[ch2] = [C]\n[polymer_chain] = repeat([ch2], 5)'
165166
const program = parse(source)
166167
expect(resolve(program, 'polymer_chain')).toBe('[C][C][C][C][C]')
167168
})
168169

169170
test('polymer chain with decode', () => {
170-
const source = '[ch2] = [C]\n[polymer_chain] = repeat(\'[ch2]\', 5)'
171+
const source = '[ch2] = [C]\n[polymer_chain] = repeat([ch2], 5)'
171172
const program = parse(source)
172173
expect(resolve(program, 'polymer_chain', { decode: true })).toBe('CCCCC')
173174
})
174175

175176
test('vinyl chloride monomer units', () => {
176177
// Each monomer as a branch structure for proper chemistry
177-
const source = '[monomer] = [C][Branch1][C][Cl][C]\n[polymer] = repeat(\'[monomer]\', 3)'
178+
const source = '[monomer] = [C][Branch1][C][Cl][C]\n[polymer] = repeat([monomer], 3)'
178179
const program = parse(source)
179180
// This creates a branched structure: C(Cl)CC(Cl)CC(Cl)C
180181
expect(resolve(program, 'polymer')).toBe('[C][Branch1][C][Cl][C][C][Branch1][C][Cl][C][C][Branch1][C][Cl][C]')

test/fixtures/programs/polymers.selfies

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,12 @@
3030
# Polyester building blocks
3131
[ethylene_glycol] = [C][C][hydroxyl][hydroxyl]
3232
[terephthalic_acid] = [phenyl][carboxyl][carboxyl]
33+
34+
# Using repeat() macro for polymer chains
35+
[PE_trimer] = repeat([PE_unit], 3)
36+
[PP_pentamer] = repeat([PP_unit], 5)
37+
[PS_tetramer] = repeat([PS_unit], 4)
38+
[PVC_hexamer] = repeat([PVC_unit], 6)
39+
40+
# Carbon chain using repeat
41+
[decane_chain] = repeat([C], 10)

test/fixtures/programs/reuse-patterns.selfies

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
[pentane] = [m][m][m][m][m]
1515
[hexane] = [m][m][m][m][m][m]
1616

17+
# Same patterns using repeat() macro
18+
[heptane] = repeat([m], 7)
19+
[octane] = repeat([m], 8)
20+
[nonane] = repeat([m], 9)
21+
[decane] = repeat([m], 10)
22+
1723
# ═══════════════════════════════════════════════════════════════
1824
# REUSE ACROSS MULTIPLE DEFINITIONS
1925
# ═══════════════════════════════════════════════════════════════
@@ -46,6 +52,10 @@
4652
[aaaa] = [aa][aa]
4753
[aaaaaaaa] = [aaaa][aaaa]
4854

55+
# Using repeat for longer chains
56+
[a16] = repeat([a], 16)
57+
[a32] = repeat([a], 32)
58+
4959
# ═══════════════════════════════════════════════════════════════
5060
# REUSE WITH DIFFERENT COMPOSITIONS
5161
# ═══════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)