LINUX.ORG.RU

запомнить состояние вычисления и потом вспомнить его

 


2

1

Задача опять про раскраску. Нужно запустить парсер с начала файла, периодически запоминать его состояние в тех или иных местах, чтобы можно было из этого состояния многократно перезапустить.

Вопрос: есть ли такая библиотека, которая позволяет это реализовать? При этом хочется управлять понятием «состояние». Например, поток, из которого читаю, я хочу ему каждый раз подсовывать новый. Ну и весь стек до глубины сибирских руд мне не нужен. Мне нужно состояние только от точки входа в парсер.

Также хочется, чтобы содержимое некоторых переменных копировалось при сохранении состояния вычисления, а то может произойти размазывание (smash) мутируемых данных.

★★★★★

Последнее исправление: den73 (всего исправлений: 1)

inb4 call/cc

anonymous
()

Берёшь любой парсер и втыкаешь в нужные места restart-case и error. Перед restart-case копируешь/сохраняешь состояние.

monk ★★★★★
()
Ответ на: комментарий от den73

Состояние включает в себя стек вызовов, кучу связанных локальных и несколько специальных переменных.

den73 ★★★★★
() автор топика

Например, поток, из которого читаю, я хочу ему каждый раз подсовывать новый.

Если всего лишь это, то можно что-то вроде

(defun parse-entry (stream)
  (let ((text (read-entry stream))
    (if (well-formed-log-entry-p text)
      (make-instance 'entry ...)
      (restart-case (error 'malformed-entry-error :text text)
        (use-value (value) value)
        (reparse-entry (new-stream) (parse-entry new-stream)))))

monk ★★★★★
()
Ответ на: комментарий от monk

Не, чё-то это не про то. Тут вопрос не столько в ошибках, сколько в кешировании результатов, чтобы не раскрашивать каждый раз весь файл целиком. Буду сохранять состояние парсера, скажем, каждые 100 строчек, а последние 100 строчек перезапускать. Похоже, для этого нужны продолжения. Но я уже в той теме посмотрел, что есть cl-cont и даже во многих проектах применяются - посмотрю, как они себя поведут (когда руки дойдут до этой задачи)

den73 ★★★★★
() автор топика

Ты GLR что-ли мутишь? Хрестоматийный подход вроде на форке реализуется, емнип. Поток подсовывать можно через пайп.

anonymous
()
Ответ на: комментарий от den73

Тут вопрос не столько в ошибках, сколько в кешировании результатов, чтобы не раскрашивать каждый раз весь файл целиком.

В CL это классически делается через

(defun read-token ...
  ...
  (let ((*parser-state* *parser-state*))
    (restart-case (parse-token ...)
        (use-value (value) value)
        (reparse-entry (new-stream new ...) (read-token new-stream)))))

В общем, в случае рестарта у тебя *parser-state* на момент начала парсинга элемента. А в рестарте reparse можешь передать дополнительные данные (новый поток, параметры парсинг и т.д.)

monk ★★★★★
()
Ответ на: комментарий от monk

это значит, что *parser-state* я должен чем-то сам заполнить. А я хочу, чтобы он автоматически создался из стека, локальных и специальных переменных. Ибо парсер написан методом рекурсивного спуска.

den73 ★★★★★
() автор топика
Ответ на: комментарий от monk

Чтобы было понятнее. Файл 1000 строк.

Запускаю парсер и сохраняю состояние каждые 100 строк (привязывая каждое состояние к какой-то точке в тексте). Попутно в каждой букве ставлю цвет и другие атрибуты.

При любом редактировании ищу вверх от места редактирования ближайшее состояние парсера и запускаю его из этого состояния. На входе - хвост отредактированного файла до места сохранения состояния до конца файла.

Таким образом, это что-то типа call/cc, но его нужно иметь возможность многократно запускать. Не могу умыслить, что ты предлагаешь с restart-case, во-первых, неясно, можно ли многократно запускать рестарт, во-вторых, restart-case имеет динамический как его там, время жизни. Т.е., нужно держать под это дело приостановленный тред (по треду на каждый открытый файл), и по набору рестартов на каждое сохранённое состояние. Держать тред вроде не так и страшно (хотя 50 тредов кажутся большой цифрой), а вот держать глубокий стек - не знаю, как бы он не закончился от такого использования, у меня ведь файлы и по 100000 строк пока что успешно открываются.

Или я опять тебя не понял.

В общем, я хочу call/cc и даже могу предложить способ это сделать: использовать для этой задачи интерпретатор, в котором все конструкции (стек, обработка ошибок) сделаны в рамках вирт.машины, а не с опорой на возможности компилируемой рантайм-среды. Если повезёт, это даст возможность сохранять-перезапускать.

Хотя я ещё не смотрел cl-cont, может и его хватит.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Запускаю парсер и сохраняю состояние каждые 100 строк (привязывая каждое состояние к какой-то точке в тексте).

Для этого всё равно придётся писать функцию сохранения состояния. И тогда можно будет использовать динамическую переменную.

В общем, я хочу call/cc

call/cc даёт замыкание, а не состояние всех переменных.

(defvar *state*)

(let ((x 1))
  (call/cc (lambda (k)
             (setf *state k)))
  (incf x)
  x)

(funcall *state*) ; => 3
(funcall *state*) ; => 4
(funcall *state*) ; => 5

Обрати внимание, что значение x не восстанавливается.

А глобальное состояние (позиции в потоках, стек динамических переменных, стек выполнения и т.д.) в CL получить нельзя.

использовать для этой задачи интерпретатор, в котором все конструкции (стек, обработка ошибок) сделаны в рамках вирт.машины

Можно. Только, по моему мнению, проще написать запись состояния в переменную. В конце концов, переопределить setf, defun и read-line (ну или чем поток читаешь) несложно.

monk ★★★★★
()
Ответ на: комментарий от monk

Обрати внимание, что значение x не восстанавливается.

Кстати динамические переменные, кажется, восстанавливаются.

anonymous
()
Ответ на: комментарий от monk

Ну, если наложить ограничения типа «не делать setf», то замыкание = состояние. Правда, это не мой случай, setf там на каждом шагу.

Состояние вычисления - это:

локальные значения переменных во всех кадрах точки вызова (что и откуда) в каждом кадре все установленные хендлеры все связанные локальные переменные

Код примерно 1500 строк, да ещё и с dsl-ем. Да ещё и не один (разные моды для разных языков).

В одуванчике сделано через виртмашину, отсюда и мысль.

https://bitbucket.org/budden/oduvanchik/src/default/src/

И там три файла: exp-syntax-vm.lisp, exp-syntax-macros.lisp, exp-syntax.lisp.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Позиции в потоках не интересуют. Часть остального глобального состояния - интересует.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

то замыкание = состояние.

Так а завернуть в лямбду просто почему нельзя?

anonymous
()
Ответ на: комментарий от den73

локальные значения переменных во всех кадрах точки вызова (что и откуда) в каждом кадре все установленные хендлеры все связанные локальные переменные

Переопредели макросами let, defun, setf, чтобы они при выполнении записывали состояние.

monk ★★★★★
()
Ответ на: комментарий от monk

локальные значения переменных во всех кадрах точки вызова (что и откуда) в каждом кадре все установленные хендлеры все связанные локальные переменные

Переопредели макросами let, defun, setf, чтобы они при выполнении записывали состояние.

Ну и это и есть интерпретатор написать, разве нет?

Я подумал ещё один вариант, можно запоминать состояния с помощью отладчика, вызывая его программно. Ну и плюс CPS преобразование. Хотя это хрупко, зависит от реализации и может вовсе не работать. Например, если в функции две одноимённых переменных (let ((a a)) ...), то у SBCL уже проблемы.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Ну и это и есть интерпретатор написать, разве нет?

Нет. Интерпретатор, это когда ты пишешь (my-lang '(здесь твой код)). А пара макросов никак на интерпретатор не тянут.

Ну и плюс CPS преобразование. Хотя это хрупко, зависит от реализации и может вовсе не работать.

cl-cont, насколько я знаю достаточно стабильно работает.

Например, если в функции две одноимённых переменных (let ((a a)) ...), то у SBCL уже проблемы.

(let ((a a)) ...) === (funcall (lambda (a) ...) a), а последнее CPS-иться тривиально.

monk ★★★★★
()
Ответ на: комментарий от monk

Я имел в виду, сохранять состояние вычисления с помощью дебаггера - это хрупко, а не CPS. И проблема про одноимённые переменные - это проблема именно SBCL-ного дебаггера.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Я имел в виду, сохранять состояние вычисления с помощью дебаггера - это хрупко

С этим полностью согласен. Почему не хочешь через макросы сохранять? Если достаточно состояний на входе в функцию, то вообще нужно только defun переопределить (или написать свой аналог trace).

monk ★★★★★
()
Ответ на: комментарий от monk

Не понял, как именно я должен переопределить defun

(defun parse-function (stream) 
  (parse-function-keyword stream)
  (parse-function-name stream)
  (parse-function-arguments stream)
  (parse-function-body stream)
)

(defun parse-function-arguments (stream)
  (parse-opening-paren stream)
  (parse-maybe-function-arguments-no-parens stream)
  (parse-opening-paren stream)
)

Во что должен развенуться мой изменённый defun, учитывая, что я могу прервать прервать процесс перед любым чтением?

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Как я могу понять, этот defun должен идентифицировать каждое место, где он вызывает следующую функцию. В этом месте я должен делать разрыв функции в CPS, чтобы потом можно было вернуться в это место.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73
(defmacro defun (name args &rest body)
  `(defun ,name ,args
     ,@(loop for a in args `(save-arg *state* ',a ,a ,name))
     (restart-case (progn ,@body)
        (do-smth (cont)
           (load-state *state*)
           (cont)))))
monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

Это ты ещё вчера предлагал. Я сказал, что тогда понадобится уровень вложенности стека на каждое запомненное состояние (для хранения точки рестарта) и плюс отдельный тред на каждый буфер. Я это верно понимаю? Ты не ответил. Ещё я спрашивал, можно ли рестарт вызвать более одного раза.

Видимо, я сам могу ответить - скорее всего нельзя, но ничто не мешает первым делом при вызове создать такой же рестарт снова взамен использованного.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Я сказал, что тогда понадобится уровень вложенности стека на каждое запомненное состояние (для хранения точки рестарта)

Так у тебя и так он есть при вызове процедуры. И рестарт на каждую процедуру. Зачем тред?

monk ★★★★★
()
Ответ на: комментарий от monk

Так у тебя и так он есть при вызове процедуры.

Если я разбираю файл рекурсивным парсером, то уровень вложенности равен, грубо говоря, глубине вложенности скобок в файле.

А при твоём предложении получается, что состояние остаётся на стеке (чтобы его оттуда не доставать) и что если я хочу сохранять состояние парсера каждые 100 строк, то глубина вложенности в файле 100.000 строк будет (1000*глубина вложенности скобок в файле), а это уже несколько другое дело. Не так разве?

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

А тред нужен для того, что у меня много файлов. Или ты хочешь сказать, что я могу этот restart-case как-то с лямбдой скрестить, чтобы записать замыкание в переменную, живущую в данном буфере? invoke-restart-то я откуда смогу вызвать, чтобы вернуться в запомненное прошлое?

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от den73

invoke-restart-то я откуда смогу вызвать, чтобы вернуться в запомненное прошлое?

Из handler-bind, вестимо. А тот уже может откуда угодно данные брать — хоть из соседнего потока, хоть ещё откуда.

если я хочу сохранять состояние парсера каждые 100 строк

Я предполагал сохранение при каждом входе в функцию — тогда количество сохранений = максимальной вложенности скобок.

А каждые 100 строк можно так:

(defun (read-rest n ...)
  (read-line ...)
  (parse-line ...)
  (if (= (mod n 100) 0)
      (let ((*state* (save-state))
         (restart-case (read-rest (+ n 1) ...) ...))
      (read-rest (+ n 1) ...)))
monk ★★★★★
()
Ответ на: комментарий от monk

И глубина вложенности будет 1000+количество скобок

monk ★★★★★
()
Ответ на: комментарий от monk

Ладно, я подумаю. Может, так и прокатит. Спасибо! Если не прокатит - посмотрю ещё раз в сторону интерпретатора.

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 2)
24 марта 2016 г.
Ответ на: комментарий от monk

Я вернулся к этой задаче и понял, как всё плохо. Состояние парсера - это структуры, которые он успел построить до данной точки. Я сначала создаю эти структуры пустыми, потом заполняю их. Чтобы запомнить состояние, нужно либо сделать глубокую копию всего, что создано на данный момент, либо в ФП стиле каждое присваивание заменить копированием. Есть и гибридные варианты (с двухкратным присваиванием, хранением истории значений и т.п.), но в любом случае получается что-то довольно устрашающее.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

А зачем вообще это надо? Может, задачу можно решить проще?

anonymous
()
Ответ на: комментарий от den73

Чтобы запомнить состояние, нужно либо сделать глубокую копию всего, что создано на данный момент

Разумеется. А чем это плохо? Памяти не хватает?

monk ★★★★★
()
Ответ на: комментарий от den73

либо в ФП стиле каждое присваивание заменить копированием

Так как каждое, то проще будет сразу на Haskell переписать

monk ★★★★★
()
Ответ на: комментарий от monk

Тем, что это медленно. Насчёт памяти пока не знаю.

den73 ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.