Skip to content

Commit 328580d

Browse files
flang: выборочный импорт «только» — чтобы не дублировать чужой код
Импорт вносил все имена модуля, и достаточно было одного совпадения, чтобы связывание отказало с FLANG_DUPLICATE_NAME. Цена выяснилась сразу: лексеру ядра пришлось скопировать к себе четыре тотальные функции stdlib из-за одной одноимённой — «Обрезать пробелы», которая в библиотеке обычная, а в лексере обязана быть тотальной. `использует «Списки» из "…" только «Сумма», «Длина»` берёт названное и оставляет остальное снаружи. Просьба не расширяет видимое: имя, не попавшее в «экспортирует», не придёт и по «только» — иначе экспорт перестал бы что-либо значить. Проверено: 870 тестов flang, 0 падений.
1 parent af52ac4 commit 328580d

4 files changed

Lines changed: 126 additions & 2 deletions

File tree

flang/src/lexer.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export const KEYWORDS = {
8686
and: ["и", "and"],
8787
with: ["с", "with"],
8888
from: ["из", "from"],
89+
only: ["только", "only"],
8990
to: ["в", "к", "to", "into", "in", "onto"],
9091
by: ["по", "by"],
9192
at: ["у", "at"],

flang/src/link.mjs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ export async function linkProgram(entryFile, source, parse, options = {}) {
6363
const sources = new Map()
6464
const withImports = new Set()
6565

66+
/**
67+
* Пересечение «что модуль отдаёт» и «что у него просят».
68+
*
69+
* Просьба не расширяет видимое: `только «Внутри»` для неэкспортированного
70+
* имени ничего не даёт — иначе `экспортирует` перестал бы что-либо значить.
71+
*/
72+
const narrow = (exported, requested) => {
73+
if (requested === undefined || requested === null) return exported
74+
if (exported === null) return requested
75+
return new Set([...requested].filter((name) => exported.has(name)))
76+
}
77+
6678
const merge = (program, file, visible) => {
6779
for (const type of program.types ?? []) {
6880
if (visible !== null && !visible.has(type.name)) continue
@@ -102,6 +114,10 @@ export async function linkProgram(entryFile, source, parse, options = {}) {
102114
}
103115
}
104116

117+
/* Что именно берём из каждого файла: `null` — всё видимое, иначе набор имён
118+
из `только`. Пересечение с `экспортирует` считается в merge. */
119+
const wanted = new Map()
120+
105121
const load = async (file, text, isEntry) => {
106122
if (loaded.has(file)) return null
107123
/* Цикл импортов — ошибка, а не молчаливая остановка: иначе часть объявлений
@@ -155,6 +171,12 @@ export async function linkProgram(entryFile, source, parse, options = {}) {
155171
)
156172
continue
157173
}
174+
if (Array.isArray(entry.only)) {
175+
const already = wanted.get(target)
176+
wanted.set(target, already === undefined ? new Set(entry.only) : new Set([...already, ...entry.only]))
177+
} else {
178+
wanted.set(target, null)
179+
}
158180
const child = await load(target, imported, false)
159181
/* Имя модуля в `использует` обязано совпадать с его заголовком: иначе
160182
читатель видит одно имя, а получает объявления из другого файла. */
@@ -170,7 +192,7 @@ export async function linkProgram(entryFile, source, parse, options = {}) {
170192

171193
loading.pop()
172194
loaded.add(file)
173-
merge(program, file, isEntry ? null : exportsOf(program))
195+
merge(program, file, isEntry ? null : narrow(exportsOf(program), wanted.get(file)))
174196
return program
175197
}
176198

flang/src/parser.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,18 @@ class Parser {
568568
const category = this.expectName("ожидалось имя категории")
569569
this.expectKw("from", "после имени категории ожидалось 'из'")
570570
const from = this.expectName("ожидался путь")
571-
value.imports.push({ category, from })
571+
/* `только «А», «Б»` — выборочный импорт. Без него модуль вносит все свои
572+
имена, и достаточно одного совпадения, чтобы связывание отказало:
573+
именно так лексеру ядра пришлось дублировать четыре функции stdlib
574+
из-за одной одноимённой. Выбор имён — способ сказать «мне нужна вот
575+
эта функция», не переименовывая чужой модуль. */
576+
const entry = { category, from }
577+
if (this.eatKw("only")) {
578+
const names = [this.expectName("ожидалось имя, которое нужно импортировать")]
579+
while (this.eatPunct(",")) names.push(this.expectName("ожидалось имя, которое нужно импортировать"))
580+
entry.only = names
581+
}
582+
value.imports.push(entry)
572583
this.endLine()
573584
return
574585
}

flang/test/link.test.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,96 @@ test("у входного файла видно всё, даже если он
297297
assert.deepEqual(names.sort(), ["Внутри", "Наружу"])
298298
})
299299

300+
/* ─────────────────────────── выборочный импорт ──────────────────────────── */
301+
302+
test("«только» берёт названные имена и не тащит остальные", async () => {
303+
const dir = await sandbox({
304+
"решение.flang": `модуль «Решение»
305+
использует «Списки» из ${JSON.stringify(join(stdlib, "lists.flang"))} только «Сумма», «Длина»
306+
307+
тотальная функция «Среднее»
308+
принимает элементы: список числа
309+
возвращает число
310+
«Сумма» от элементы делить на «Длина» от элементы
311+
`,
312+
})
313+
const result = await link(join(dir, "решение.flang"), await readEntry(dir, "решение.flang"))
314+
assert.deepEqual(result.functions.map((fn) => fn.name).sort(), ["Длина", "Среднее", "Сумма"])
315+
316+
const { stdout } = await run("node", [
317+
cli,
318+
"run",
319+
join(dir, "решение.flang"),
320+
"--function",
321+
"Среднее",
322+
"--args",
323+
JSON.stringify({ элементы: [2, 4, 6] }),
324+
])
325+
assert.equal(JSON.parse(stdout).result, 4)
326+
})
327+
328+
test("«только» снимает конфликт: одноимённую функцию можно не брать", async () => {
329+
const dir = await sandbox({
330+
"чужой.flang": `модуль «Чужой»
331+
332+
тотальная функция «Нужная»
333+
возвращает число
334+
1
335+
336+
тотальная функция «Общая»
337+
возвращает число
338+
2
339+
`,
340+
"главный.flang": `модуль «Главный»
341+
использует «Чужой» из "чужой.flang" только «Нужная»
342+
343+
тотальная функция «Общая»
344+
возвращает число
345+
3
346+
347+
тотальная функция «Позвать»
348+
возвращает число
349+
«Нужная» плюс «Общая»
350+
`,
351+
})
352+
const result = await link(join(dir, "главный.flang"), await readEntry(dir, "главный.flang"))
353+
assert.deepEqual(
354+
result.diagnostics,
355+
[],
356+
`конфликта быть не должно: одноимённая функция не импортировалась, получено ${JSON.stringify(result.diagnostics)}`,
357+
)
358+
const { stdout } = await run("node", [cli, "run", join(dir, "главный.flang"), "--function", "Позвать", "--args", "{}"])
359+
assert.equal(JSON.parse(stdout).result, 4)
360+
})
361+
362+
test("«только» не расширяет видимое сверх «экспортирует»", async () => {
363+
const dir = await sandbox({
364+
"модуль.flang": `модуль «Модуль»
365+
экспортирует «Наружу»
366+
367+
тотальная функция «Наружу»
368+
возвращает число
369+
1
370+
371+
тотальная функция «Внутри»
372+
возвращает число
373+
2
374+
`,
375+
"главный.flang": `модуль «Главный»
376+
использует «Модуль» из "модуль.flang" только «Внутри»
377+
378+
тотальная функция «Ноль»
379+
возвращает число
380+
0
381+
`,
382+
})
383+
const result = await link(join(dir, "главный.flang"), await readEntry(dir, "главный.flang"))
384+
assert.ok(
385+
!result.functions.some((fn) => fn.name === "Внутри"),
386+
"неэкспортированное имя не должно приходить по просьбе",
387+
)
388+
})
389+
300390
/* ──────────────────── файл без импортов не меняется вовсе ───────────────── */
301391

302392
test("без «использует» разбор остаётся прежним: связывание ничего не подменяет", async () => {

0 commit comments

Comments
 (0)