simdref 0.0.4__tar.gz → 0.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {simdref-0.0.4/src/simdref.egg-info → simdref-0.0.5}/PKG-INFO +4 -3
  2. {simdref-0.0.4 → simdref-0.0.5}/README.md +3 -2
  3. {simdref-0.0.4 → simdref-0.0.5}/pyproject.toml +1 -1
  4. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/annotate.py +120 -33
  5. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/cli.py +219 -57
  6. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/manpages.py +13 -15
  7. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/adapters/perf.py +46 -7
  8. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/hotloop.py +70 -29
  9. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/orchestrate.py +39 -17
  10. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/storage.py +68 -18
  11. {simdref-0.0.4 → simdref-0.0.5/src/simdref.egg-info}/PKG-INFO +4 -3
  12. {simdref-0.0.4 → simdref-0.0.5}/src/simdref.egg-info/SOURCES.txt +2 -0
  13. {simdref-0.0.4 → simdref-0.0.5}/tests/test_coverage_live.py +2 -1
  14. {simdref-0.0.4 → simdref-0.0.5}/tests/test_coverage_parity.py +2 -2
  15. {simdref-0.0.4 → simdref-0.0.5}/tests/test_lsp_web.py +3 -6
  16. simdref-0.0.5/tests/test_riscv_docs_pages.py +52 -0
  17. simdref-0.0.5/tests/test_storage_payload.py +56 -0
  18. {simdref-0.0.4 → simdref-0.0.5}/tests/test_storage_schema.py +3 -3
  19. {simdref-0.0.4 → simdref-0.0.5}/tests/test_web_e2e.py +3 -2
  20. {simdref-0.0.4 → simdref-0.0.5}/LICENSE +0 -0
  21. {simdref-0.0.4 → simdref-0.0.5}/setup.cfg +0 -0
  22. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/__init__.py +0 -0
  23. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/__main__.py +0 -0
  24. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/arm_instructions.py +0 -0
  25. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/display.py +0 -0
  26. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/filters.py +0 -0
  27. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/ingest.py +0 -0
  28. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/ingest_catalog.py +0 -0
  29. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/ingest_pdf.py +0 -0
  30. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/ingest_sources.py +0 -0
  31. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/lsp.py +0 -0
  32. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/models.py +0 -0
  33. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/pdfparse/__init__.py +0 -0
  34. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/pdfparse/base.py +0 -0
  35. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/pdfparse/intel.py +0 -0
  36. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/pdfparse/registry.py +0 -0
  37. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/pdfparse/types.py +0 -0
  38. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/pdfrefs.py +0 -0
  39. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/perf.py +0 -0
  40. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/perf_sources/__init__.py +0 -0
  41. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/perf_sources/cores.py +0 -0
  42. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/perf_sources/llvm_mca.py +0 -0
  43. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  44. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/perf_sources/merge.py +0 -0
  45. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/__init__.py +0 -0
  46. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/adapters/__init__.py +0 -0
  47. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/adapters/exegesis.py +0 -0
  48. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/adapters/mca.py +0 -0
  49. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/adapters/uprof.py +0 -0
  50. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/adapters/vtune.py +0 -0
  51. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/adapters/xctrace.py +0 -0
  52. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/merge.py +0 -0
  53. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/model.py +0 -0
  54. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/profile/registry.py +0 -0
  55. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/queries.py +0 -0
  56. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/riscv.py +0 -0
  57. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/search.py +0 -0
  58. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/templates/__init__.py +0 -0
  59. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/templates/app.js +0 -0
  60. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/templates/favicon.svg +0 -0
  61. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/templates/index.html +0 -0
  62. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/templates/logo.svg +0 -0
  63. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/templates/style.css +0 -0
  64. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/tui.py +0 -0
  65. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/ui_labels.py +0 -0
  66. {simdref-0.0.4 → simdref-0.0.5}/src/simdref/web.py +0 -0
  67. {simdref-0.0.4 → simdref-0.0.5}/src/simdref.egg-info/dependency_links.txt +0 -0
  68. {simdref-0.0.4 → simdref-0.0.5}/src/simdref.egg-info/entry_points.txt +0 -0
  69. {simdref-0.0.4 → simdref-0.0.5}/src/simdref.egg-info/requires.txt +0 -0
  70. {simdref-0.0.4 → simdref-0.0.5}/src/simdref.egg-info/top_level.txt +0 -0
  71. {simdref-0.0.4 → simdref-0.0.5}/tests/test_annotate.py +0 -0
  72. {simdref-0.0.4 → simdref-0.0.5}/tests/test_audit_coverage.py +0 -0
  73. {simdref-0.0.4 → simdref-0.0.5}/tests/test_auto_update_on_version_change.py +0 -0
  74. {simdref-0.0.4 → simdref-0.0.5}/tests/test_cli_bare_query.py +0 -0
  75. {simdref-0.0.4 → simdref-0.0.5}/tests/test_cli_bootstrap_progress.py +0 -0
  76. {simdref-0.0.4 → simdref-0.0.5}/tests/test_cli_help.py +0 -0
  77. {simdref-0.0.4 → simdref-0.0.5}/tests/test_cli_llm.py +0 -0
  78. {simdref-0.0.4 → simdref-0.0.5}/tests/test_display.py +0 -0
  79. {simdref-0.0.4 → simdref-0.0.5}/tests/test_filters.py +0 -0
  80. {simdref-0.0.4 → simdref-0.0.5}/tests/test_ingest_sources.py +0 -0
  81. {simdref-0.0.4 → simdref-0.0.5}/tests/test_intel_operation_and_url.py +0 -0
  82. {simdref-0.0.4 → simdref-0.0.5}/tests/test_issue2_fixes.py +0 -0
  83. {simdref-0.0.4 → simdref-0.0.5}/tests/test_models.py +0 -0
  84. {simdref-0.0.4 → simdref-0.0.5}/tests/test_pdfparse.py +0 -0
  85. {simdref-0.0.4 → simdref-0.0.5}/tests/test_perf.py +0 -0
  86. {simdref-0.0.4 → simdref-0.0.5}/tests/test_perf_sources.py +0 -0
  87. {simdref-0.0.4 → simdref-0.0.5}/tests/test_presentation.py +0 -0
  88. {simdref-0.0.4 → simdref-0.0.5}/tests/test_preset_persistence.py +0 -0
  89. {simdref-0.0.4 → simdref-0.0.5}/tests/test_search.py +0 -0
  90. {simdref-0.0.4 → simdref-0.0.5}/tests/test_search_index_js.py +0 -0
  91. {simdref-0.0.4 → simdref-0.0.5}/tests/test_search_pushdown.py +0 -0
  92. {simdref-0.0.4 → simdref-0.0.5}/tests/test_source_kind_filter.py +0 -0
  93. {simdref-0.0.4 → simdref-0.0.5}/tests/test_source_validation.py +0 -0
  94. {simdref-0.0.4 → simdref-0.0.5}/tests/test_tui.py +0 -0
  95. {simdref-0.0.4 → simdref-0.0.5}/tests/test_ui_labels_parity.py +0 -0
  96. {simdref-0.0.4 → simdref-0.0.5}/tests/test_version_matches_pyproject.py +0 -0
  97. {simdref-0.0.4 → simdref-0.0.5}/tests/test_x86_linking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simdref
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, LSP, and static web export.
5
5
  Author: Marco
6
6
  License: GPL-3.0-or-later
@@ -39,8 +39,9 @@ Dynamic: license-file
39
39
  A single searchable reference for SIMD intrinsics and instructions across
40
40
  **x86 (Intel + uops.info)**, **Arm (ACLE / AARCHMRS)**, and **RISC-V
41
41
  (RVV + unified-db)**. Runs as a CLI, a Textual TUI, an LSP server,
42
- generated manpages, a static web app, and a structured JSON interface
43
- for LLM skills.
42
+ on-demand manpages (`simdref man` — or run `simdref install-manpages`
43
+ so plain `man vpaddd` works), a static web app, and a structured JSON
44
+ interface for LLM skills.
44
45
 
45
46
  [Web App](https://diamondinoia.github.io/simdref/) ·
46
47
  [TestPyPI](https://test.pypi.org/project/simdref/) ·
@@ -7,8 +7,9 @@
7
7
  A single searchable reference for SIMD intrinsics and instructions across
8
8
  **x86 (Intel + uops.info)**, **Arm (ACLE / AARCHMRS)**, and **RISC-V
9
9
  (RVV + unified-db)**. Runs as a CLI, a Textual TUI, an LSP server,
10
- generated manpages, a static web app, and a structured JSON interface
11
- for LLM skills.
10
+ on-demand manpages (`simdref man` — or run `simdref install-manpages`
11
+ so plain `man vpaddd` works), a static web app, and a structured JSON
12
+ interface for LLM skills.
12
13
 
13
14
  [Web App](https://diamondinoia.github.io/simdref/) ·
14
15
  [TestPyPI](https://test.pypi.org/project/simdref/) ·
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simdref"
7
- version = "0.0.4"
7
+ version = "0.0.5"
8
8
  description = "Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, LSP, and static web export."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -42,23 +42,59 @@ class AsmLine:
42
42
  source_line: int | None = None
43
43
 
44
44
 
45
+ # Instruction head without trailing comment: the '#' comment (and GAS-style
46
+ # annotations) is split off with str.partition — per the regex HOWTO, string
47
+ # methods beat a lazy re group for a fixed single-character cut.
45
48
  _INSTR_RE = re.compile(
46
49
  r"^(?P<indent>[ \t]*)"
47
50
  r"(?P<mnemonic>[A-Za-z][A-Za-z0-9_.]*)"
48
- r"(?:[ \t]+(?P<operands>[^#\n]*?))?"
49
- r"(?:[ \t]*(?P<comment>#.*))?$"
51
+ r"(?:[ \t]+(?P<operands>.*))?$"
50
52
  )
51
53
  _LABEL_RE = re.compile(r"^[ \t]*[A-Za-z_.$][\w.$]*:")
52
54
  # objdump -d line shape: optional whitespace, hex VA, ':', hex bytes, mnemonic ops.
55
+ # Byte tokens are 2-8 hex digits: x86 emits one whitespace-separated token per
56
+ # byte ("48 8b 45 f8"), ARM/RISC-V emit the whole encoding as one token
57
+ # ("a9bf7bfd" / "00050513").
53
58
  _OBJDUMP_INSTR_RE = re.compile(
54
59
  r"^\s*(?P<addr>[0-9a-fA-F]+):\s+"
55
- r"(?:(?:[0-9a-fA-F]{2}\s+){1,10})?"
60
+ r"(?:(?:[0-9a-fA-F]{2,8}\s+){1,10})?"
56
61
  r"(?P<rest>\S.*?)\s*$"
57
62
  )
58
63
  # objdump -S injects "file:line" comment lines before the instruction block.
59
64
  _OBJDUMP_SRC_RE = re.compile(r"^\s*(?P<file>[^ \t/][^:]*):(?P<line>\d+)\s*$")
60
65
 
61
66
 
67
+ def _parse_objdump_instr(line: str) -> tuple[int, str] | None:
68
+ """Match an ``objdump -d`` instruction line: ``<hexaddr>: [bytes] mnemonic ...``.
69
+
70
+ Returns ``(address, rest)`` where ``rest`` starts at the mnemonic, or None
71
+ when the line is not objdump-shaped (e.g. a column-0 numeric local label
72
+ ``1:`` does not match).
73
+ """
74
+ # Cheap str gates before the regex (HOWTO: "use string methods"): objdump
75
+ # never starts an instruction line at column 0 (GAS local labels ``1:``
76
+ # do), addresses start with a hex char, and the mandatory ':' sits within
77
+ # the first 24 columns.
78
+ if not line or line[0] not in " \t":
79
+ return None
80
+ bare = line.lstrip(" \t")
81
+ if not bare or bare[0] not in "0123456789abcdefABCDEF" or ":" not in bare[:24]:
82
+ return None
83
+ obj_m = _OBJDUMP_INSTR_RE.match(line)
84
+ if not obj_m:
85
+ return None
86
+ try:
87
+ return int(obj_m.group("addr"), 16), obj_m.group("rest")
88
+ except ValueError:
89
+ return None
90
+
91
+
92
+ def _split_comment(text: str) -> tuple[str, str]:
93
+ """Split AT&T ``#`` trailing comment off; returns (head, comment)."""
94
+ head, sep, comment = text.partition("#")
95
+ return head, (sep + comment if sep else "")
96
+
97
+
62
98
  def parse_asm_line(line: str, *, track_positions: bool = False) -> AsmLine:
63
99
  stripped = line.rstrip("\n")
64
100
  if not stripped.strip():
@@ -68,17 +104,16 @@ def parse_asm_line(line: str, *, track_positions: bool = False) -> AsmLine:
68
104
  return AsmLine(LineKind.COMMENT, stripped)
69
105
  if bare.startswith("."):
70
106
  return AsmLine(LineKind.DIRECTIVE, stripped)
107
+ if bare.startswith("Disassembly of "):
108
+ # objdump section banner: "Disassembly of section .text:"
109
+ return AsmLine(LineKind.COMMENT, stripped)
71
110
 
72
- address: int | None = None
73
111
  if track_positions:
74
- obj_m = _OBJDUMP_INSTR_RE.match(stripped)
75
- if obj_m:
76
- try:
77
- address = int(obj_m.group("addr"), 16)
78
- except ValueError:
79
- address = None
80
- rest = obj_m.group("rest")
81
- m = _INSTR_RE.match(rest)
112
+ obj = _parse_objdump_instr(stripped)
113
+ if obj is not None:
114
+ address, rest = obj
115
+ head, comment = _split_comment(rest)
116
+ m = _INSTR_RE.match(head)
82
117
  if m:
83
118
  return AsmLine(
84
119
  kind=LineKind.INSTRUCTION,
@@ -86,7 +121,7 @@ def parse_asm_line(line: str, *, track_positions: bool = False) -> AsmLine:
86
121
  indent=(stripped[: stripped.find(rest)] if rest in stripped else ""),
87
122
  mnemonic=m.group("mnemonic") or "",
88
123
  operands=(m.group("operands") or "").strip(),
89
- trailing_comment=(m.group("comment") or "").strip(),
124
+ trailing_comment=comment.strip(),
90
125
  address=address,
91
126
  )
92
127
  # In objdump mode, any non-matching line is an objdump header,
@@ -98,7 +133,27 @@ def parse_asm_line(line: str, *, track_positions: bool = False) -> AsmLine:
98
133
  if _LABEL_RE.match(stripped):
99
134
  return AsmLine(LineKind.LABEL, stripped)
100
135
 
101
- m = _INSTR_RE.match(stripped)
136
+ # objdump-style input (workflow §1d: `objdump -d ... > file.s`) is a
137
+ # first-class input even without --track-positions: strip the leading
138
+ # " <hexaddr>:\t" (and any raw bytes column) so the mnemonic parses.
139
+ obj = _parse_objdump_instr(stripped)
140
+ if obj is not None:
141
+ address, rest = obj
142
+ head, comment = _split_comment(rest)
143
+ m = _INSTR_RE.match(head)
144
+ if m:
145
+ return AsmLine(
146
+ kind=LineKind.INSTRUCTION,
147
+ raw=stripped,
148
+ indent=(stripped[: stripped.find(rest)] if rest in stripped else ""),
149
+ mnemonic=m.group("mnemonic") or "",
150
+ operands=(m.group("operands") or "").strip(),
151
+ trailing_comment=comment.strip(),
152
+ address=address,
153
+ )
154
+
155
+ head, comment = _split_comment(stripped)
156
+ m = _INSTR_RE.match(head)
102
157
  if not m:
103
158
  return AsmLine(LineKind.COMMENT, stripped)
104
159
  return AsmLine(
@@ -107,8 +162,8 @@ def parse_asm_line(line: str, *, track_positions: bool = False) -> AsmLine:
107
162
  indent=m.group("indent") or "",
108
163
  mnemonic=m.group("mnemonic") or "",
109
164
  operands=(m.group("operands") or "").strip(),
110
- trailing_comment=(m.group("comment") or "").strip(),
111
- address=address,
165
+ trailing_comment=comment.strip(),
166
+ address=None,
112
167
  )
113
168
 
114
169
 
@@ -614,9 +669,23 @@ def _annotate_instruction(
614
669
  parsed: AsmLine,
615
670
  opts: AnnotateOptions,
616
671
  conn: sqlite3.Connection,
672
+ lookup_cache: dict[str, list[InstructionRecord]] | None = None,
673
+ stats: dict[str, int] | None = None,
617
674
  ) -> tuple[str, dict[str, Any] | None]:
618
675
  """Return the rendered output line and an optional JSON record."""
619
- records = lookup(parsed.mnemonic, conn)
676
+ if lookup_cache is not None:
677
+ records = lookup_cache.get(parsed.mnemonic)
678
+ if records is None:
679
+ # Module-level call (not a captured reference) so tests can
680
+ # keep monkeypatching ``annotate.lookup`` with a 2-arg fake.
681
+ records = lookup(parsed.mnemonic, conn)
682
+ lookup_cache[parsed.mnemonic] = records
683
+ else:
684
+ records = lookup(parsed.mnemonic, conn)
685
+ if stats is not None:
686
+ stats["parsed"] = stats.get("parsed", 0) + 1
687
+ if records:
688
+ stats["recognized"] = stats.get("recognized", 0) + 1
620
689
  record = pick_record(records, arch=opts.arch, operands=parsed.operands)
621
690
 
622
691
  if record is None:
@@ -693,34 +762,52 @@ def annotate_stream(
693
762
  *,
694
763
  opts: AnnotateOptions,
695
764
  conn: sqlite3.Connection,
765
+ stats: dict[str, int] | None = None,
696
766
  ) -> Iterator[str]:
697
- """Yield annotated lines for each input line (newline-terminated)."""
767
+ """Yield annotated lines for each input line (newline-terminated).
768
+
769
+ ``stats`` (optional) accumulates parse/recognition counters — keys
770
+ ``parsed``, ``recognized``, ``content`` — so callers can warn when the
771
+ input format was not understood (e.g. 0 of N instruction lines found).
772
+ """
698
773
  json_records: list[dict[str, Any]] = []
699
774
  collecting_json = opts.fmt == "json"
700
775
  pending_src_file: str | None = None
701
776
  pending_src_line: int | None = None
777
+ lookup_cache: dict[str, list[InstructionRecord]] = {}
702
778
 
703
779
  for line in lines:
704
- if opts.track_positions:
705
- src_m = _OBJDUMP_SRC_RE.match(line.rstrip("\n"))
706
- if src_m:
707
- pending_src_file = src_m.group("file").strip()
708
- try:
709
- pending_src_line = int(src_m.group("line"))
710
- except ValueError:
711
- pending_src_line = None
712
- if not collecting_json:
713
- yield line if line.endswith("\n") else line + "\n"
714
- continue
715
780
  parsed = parse_asm_line(line, track_positions=opts.track_positions)
716
- if opts.track_positions and parsed.kind == LineKind.INSTRUCTION:
717
- parsed.source_file = pending_src_file
718
- parsed.source_line = pending_src_line
781
+ if opts.track_positions:
782
+ if parsed.kind == LineKind.INSTRUCTION:
783
+ parsed.source_file = pending_src_file
784
+ parsed.source_line = pending_src_line
785
+ else:
786
+ # `-S` source interleave: objdump injects "file:line" marker
787
+ # lines immediately before the block they describe. Only
788
+ # non-instruction lines reach the (pricier) marker regex.
789
+ src_m = _OBJDUMP_SRC_RE.match(parsed.raw)
790
+ if src_m:
791
+ pending_src_file = src_m.group("file").strip()
792
+ try:
793
+ pending_src_line = int(src_m.group("line"))
794
+ except ValueError:
795
+ pending_src_line = None
796
+ if not collecting_json:
797
+ yield parsed.raw + "\n"
798
+ continue
799
+ if stats is not None and parsed.kind != LineKind.BLANK:
800
+ # "content" = anything that is not blank, an assembler directive,
801
+ # or a real comment — i.e. instructions, labels, and unrecognised
802
+ # lines the parser rejected (so "recognised 0/N" can warn).
803
+ s = parsed.raw.lstrip()
804
+ if s and not s.startswith((".", "#", "//")):
805
+ stats["content"] = stats.get("content", 0) + 1
719
806
  if parsed.kind != LineKind.INSTRUCTION:
720
807
  if not collecting_json:
721
808
  yield parsed.raw + "\n"
722
809
  continue
723
- out_line, record = _annotate_instruction(parsed, opts, conn)
810
+ out_line, record = _annotate_instruction(parsed, opts, conn, lookup_cache, stats)
724
811
  if collecting_json:
725
812
  if record is not None:
726
813
  json_records.append(record)