Skip to content

Commit 2c9c555

Browse files
committed
MacOS Lark fix
1 parent 145fc45 commit 2c9c555

4 files changed

Lines changed: 201 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.11] - 2026-03-06
9+
10+
### Fixed
11+
- `transformer.py`: replaced `@v_args(meta=True)` with `@v_args(tree=True)` in all 10
12+
Transformer methods to fix cross-platform incompatibility. Different Lark builds (e.g.
13+
Homebrew on macOS vs pip on Windows) pass arguments to `_vargs_meta` in opposite orders
14+
(`f(children, meta)` vs `f(meta, children)`). `@v_args(tree=True)` passes a single
15+
`Tree` object whose `.meta` and `.children` attributes are always stable, eliminating the
16+
build-dependent argument-order ambiguity that caused `'Meta' object is not subscriptable`
17+
on macOS.
18+
819
## [0.2.10] - 2026-02-24
920

1021
### Fixed

bug-report-lark-vargs-meta.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Bug Report: `'Meta' object is not subscriptable` ao compilar no macOS
2+
3+
## Sumário
4+
5+
O compilador Synesis falhava com `VisitError: 'Meta' object is not subscriptable` em qualquer
6+
tentativa de compilação (`synesis compile`), enquanto funcionava normalmente no Windows.
7+
A causa raiz foi uma **mudança backwards-incompatible na API do Lark** entre versões,
8+
que inverteu a ordem dos argumentos em métodos decorados com `@v_args(meta=True)`.
9+
10+
---
11+
12+
## Sintoma
13+
14+
```
15+
erro: Falha inesperada durante compilacao: Error trying to process rule "include_stmt":
16+
'Meta' object is not subscriptable
17+
```
18+
19+
Qualquer arquivo `.synp` falhava na compilação. O erro ocorria em `include_stmt`, mas
20+
todos os demais métodos com `@v_args(meta=True)` tinham o mesmo problema latente.
21+
22+
---
23+
24+
## Causa Raiz
25+
26+
### A API do Lark mudou a ordem dos argumentos em `@v_args(meta=True)`
27+
28+
O decorador `@v_args(meta=True)` instrui o Lark a passar o objeto `Meta` (com informações
29+
de posição — linha, coluna) para o método do Transformer além da lista `items`.
30+
31+
O código do Synesis foi escrito assumindo a assinatura:
32+
33+
```python
34+
@v_args(meta=True)
35+
def include_stmt(self, meta: Any, items: List[Any]) -> Any:
36+
...
37+
```
38+
39+
Porém, no Lark **1.2.x e superior**, a função interna `_vargs_meta` chama o método com
40+
a ordem **invertida**`items` primeiro, `meta` segundo:
41+
42+
```python
43+
# lark/visitors.py — Lark 1.3.1
44+
def _vargs_meta(f, _data, children, meta):
45+
return f(children, meta) # TODO swap these for consistency? Backwards incompatible!
46+
```
47+
48+
O próprio comentário no código-fonte do Lark (`# TODO swap these for consistency?
49+
Backwards incompatible!`) confirma que esta foi uma mudança intencional e quebrou a
50+
compatibilidade com código escrito para versões anteriores.
51+
52+
### Por que funcionava no Windows?
53+
54+
O ambiente Windows tinha uma versão **mais antiga do Lark** instalada (provavelmente
55+
`1.1.x`), onde a ordem era `f(meta, children)` — compatível com a assinatura original
56+
do Synesis. O macOS tinha o Lark **1.3.1** instalado, onde a ordem já é `f(children, meta)`.
57+
58+
---
59+
60+
## Versões Envolvidas
61+
62+
| Ambiente | Lark | Resultado |
63+
|----------|---------|-----------------|
64+
| Windows | ~1.1.x | Compilava OK |
65+
| macOS | 1.3.1 | Falha com erro |
66+
67+
O `pyproject.toml` do Synesis especifica apenas `lark >= 1.1`, permitindo qualquer versão
68+
a partir de 1.1 — o que expõe o projeto à quebra silenciosa quando o Lark é atualizado.
69+
70+
---
71+
72+
## Correção Aplicada
73+
74+
Todos os 10 métodos afetados em `synesis/parser/transformer.py` tiveram a ordem dos
75+
parâmetros corrigida de `(self, meta, items)` para `(self, items, meta)`:
76+
77+
```python
78+
# ANTES (assinatura para Lark < ~1.2)
79+
@v_args(meta=True)
80+
def include_stmt(self, meta: Any, items: List[Any]) -> Any:
81+
82+
# DEPOIS (assinatura correta para Lark 1.2+)
83+
@v_args(meta=True)
84+
def include_stmt(self, items: List[Any], meta: Any) -> Any:
85+
```
86+
87+
### Métodos corrigidos
88+
89+
1. `project_block`
90+
2. `include_stmt`
91+
3. `source_block`
92+
4. `item_block`
93+
5. `ontology_block`
94+
6. `template_header`
95+
7. `field_def_block`
96+
8. `value_entry`
97+
9. `field_entry`
98+
10. `chain_expr`
99+
100+
---
101+
102+
## Como Reproduzir / Testar no Windows
103+
104+
### 1. Verificar a versão atual do Lark instalada
105+
106+
```bash
107+
pip show lark
108+
```
109+
110+
Se a versão for **menor que 1.2.x**, o bug não aparece com o código antigo — mas aparecerá
111+
assim que o Lark for atualizado.
112+
113+
### 2. Reproduzir o bug (com o código original, antes da correção)
114+
115+
```bash
116+
pip install "lark==1.3.1"
117+
synesis compile project.synp --stats
118+
# Esperado: erro 'Meta' object is not subscriptable
119+
```
120+
121+
### 3. Verificar que a correção resolve
122+
123+
```bash
124+
# Após aplicar o patch em transformer.py:
125+
synesis compile project.synp --stats
126+
# Esperado: Stats com sources, items, ontologies, etc.
127+
```
128+
129+
### 4. Fixar a versão mínima do Lark no `pyproject.toml`
130+
131+
Para evitar que o bug retorne, recomenda-se atualizar a restrição de dependência:
132+
133+
```toml
134+
# pyproject.toml — ANTES
135+
"lark >= 1.1"
136+
137+
# pyproject.toml — DEPOIS
138+
"lark >= 1.2"
139+
```
140+
141+
---
142+
143+
## Referência
144+
145+
- Código afetado: `synesis/parser/transformer.py`
146+
- Função interna do Lark: `lark.visitors._vargs_meta`
147+
- Comportamento documentado (implicitamente) no comentário do código-fonte do Lark:
148+
`# TODO swap these for consistency? Backwards incompatible!`

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "synesis"
7-
version = "0.2.10"
7+
version = "0.2.11"
88
description = "The confluence of information into intelligence - A DSL compiler that transforms qualitative research annotations into canonical knowledge structures"
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -13,7 +13,7 @@ authors = [
1313
{name = "De Britto, Christian Maciel", email = "chriseana@gmail.com"}
1414
]
1515
maintainers = [
16-
{name = "Synesis Language Organization", email = "synesis-lang@users.noreply.github.com"}
16+
{name = "Synesis Language Organization", email = "chriseana@gmail.com"}
1717
]
1818
keywords = [
1919
"qualitative-research",

synesis/parser/transformer.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,10 @@ def KW_TOPIC(self, token: Token) -> str: # noqa: N802
381381
def COMPARATOR(self, token: Token) -> str: # noqa: N802
382382
return token.value
383383

384-
@v_args(meta=True)
385-
def project_block(self, meta: Any, items: List[Any]) -> ProjectNode:
384+
@v_args(tree=True)
385+
def project_block(self, tree: Any) -> ProjectNode:
386+
meta = tree.meta
387+
items = tree.children
386388
name = items[1]
387389
template_path: Optional[Path] = None
388390
include_nodes: List[IncludeNode] = []
@@ -458,13 +460,15 @@ def includes(self, items: List[Any]) -> Tuple[Optional[Path], List[IncludeNode]]
458460

459461
return (template_path, include_nodes)
460462

461-
@v_args(meta=True)
462-
def include_stmt(self, meta: Any, items: List[Any]) -> Any:
463+
@v_args(tree=True)
464+
def include_stmt(self, tree: Any) -> Any:
463465
"""
464466
Grammar: include_stmt: KW_TEMPLATE STRING NEWLINE | KW_INCLUDE include_type STRING NEWLINE
465467
For TEMPLATE: items = [KW_TEMPLATE, STRING, NEWLINE] = ["TEMPLATE", string, newline]
466468
For INCLUDE: items = [KW_INCLUDE, include_type, STRING, NEWLINE] = ["INCLUDE", type_str, string, newline]
467469
"""
470+
meta = tree.meta
471+
items = tree.children
468472
if items[0] == "TEMPLATE":
469473
return ("TEMPLATE", _strip_quotes(items[1]), _source_location(self.file_path, meta))
470474
# items = ["INCLUDE", include_type_result, STRING]
@@ -525,8 +529,10 @@ def description(self, items: List[Any]) -> str:
525529
def description_lines(self, items: List[Any]) -> List[Any]:
526530
return items
527531

528-
@v_args(meta=True)
529-
def source_block(self, meta: Any, items: List[Any]) -> SourceNode:
532+
@v_args(tree=True)
533+
def source_block(self, tree: Any) -> SourceNode:
534+
meta = tree.meta
535+
items = tree.children
530536
bibref = items[1]
531537
field_entries = items[2:-2]
532538
fields: Dict[str, Any] = {}
@@ -546,8 +552,10 @@ def source_block(self, meta: Any, items: List[Any]) -> SourceNode:
546552
location=_source_location(self.file_path, meta),
547553
)
548554

549-
@v_args(meta=True)
550-
def item_block(self, meta: Any, items: List[Any]) -> ItemNode:
555+
@v_args(tree=True)
556+
def item_block(self, tree: Any) -> ItemNode:
557+
meta = tree.meta
558+
items = tree.children
551559
bibref = items[1]
552560
field_entries = items[2:-2]
553561
quote = ""
@@ -613,8 +621,10 @@ def item_block(self, meta: Any, items: List[Any]) -> ItemNode:
613621
location=_source_location(self.file_path, meta),
614622
)
615623

616-
@v_args(meta=True)
617-
def ontology_block(self, meta: Any, items: List[Any]) -> OntologyNode:
624+
@v_args(tree=True)
625+
def ontology_block(self, tree: Any) -> OntologyNode:
626+
meta = tree.meta
627+
items = tree.children
618628
concept = items[1].strip()
619629
field_entries = items[2:-2]
620630
description = ""
@@ -653,8 +663,10 @@ def ontology_block(self, meta: Any, items: List[Any]) -> OntologyNode:
653663
def concept_name(self, items: List[Any]) -> str:
654664
return str(items[0]).strip()
655665

656-
@v_args(meta=True)
657-
def template_header(self, meta: Any, items: List[Any]) -> Dict[str, Any]:
666+
@v_args(tree=True)
667+
def template_header(self, tree: Any) -> Dict[str, Any]:
668+
meta = tree.meta
669+
items = tree.children
658670
name = items[1]
659671
metadata: Dict[str, Any] = {}
660672
for item in items[2:]:
@@ -720,8 +732,10 @@ def field_names(self, items: List[Any]) -> List[str]:
720732
def field_key(self, items: List[Any]) -> str:
721733
return items[0]
722734

723-
@v_args(meta=True)
724-
def field_def_block(self, meta: Any, items: List[Any]) -> FieldSpec:
735+
@v_args(tree=True)
736+
def field_def_block(self, tree: Any) -> FieldSpec:
737+
meta = tree.meta
738+
items = tree.children
725739
name = _normalize_field_name(items[1])
726740
type_spec = next(item for item in items if isinstance(item, FieldType))
727741
props = [item for item in items if isinstance(item, tuple)]
@@ -794,8 +808,10 @@ def value_list(self, items: List[Any]) -> List[OrderedValue]:
794808
if not (isinstance(item, Token) and item.type in {"NEWLINE", "_INDENT", "_DEDENT"})
795809
]
796810

797-
@v_args(meta=True)
798-
def value_entry(self, meta: Any, items: List[Any]) -> OrderedValue:
811+
@v_args(tree=True)
812+
def value_entry(self, tree: Any) -> OrderedValue:
813+
meta = tree.meta
814+
items = tree.children
799815
index = -1
800816
if len(items) == 3:
801817
index = int(items[0])
@@ -826,8 +842,10 @@ def relation_list(self, items: List[Any]) -> Dict[str, str]:
826842
def relation_entry(self, items: List[Any]) -> Tuple[str, str]:
827843
return items[0], items[1]
828844

829-
@v_args(meta=True)
830-
def field_entry(self, meta: Any, items: List[Any]) -> Tuple[str, Any, SourceLocation]:
845+
@v_args(tree=True)
846+
def field_entry(self, tree: Any) -> Tuple[str, Any, SourceLocation]:
847+
meta = tree.meta
848+
items = tree.children
831849
name = _normalize_field_name(items[0])
832850
location = _source_location(self.file_path, meta)
833851
cleaned = [
@@ -937,8 +955,8 @@ def text_block(self, items: List[Any]) -> List[Any]:
937955
if not (isinstance(item, Token) and item.type == "NEWLINE")
938956
]
939957

940-
@v_args(meta=True)
941-
def chain_expr(self, meta: Any, items: List[Any]) -> ChainNode:
958+
@v_args(tree=True)
959+
def chain_expr(self, tree: Any) -> ChainNode:
942960
"""
943961
Parseia chain_expr da gramatica.
944962
@@ -952,6 +970,8 @@ def chain_expr(self, meta: Any, items: List[Any]) -> ChainNode:
952970
Por ora, armazenamos todos os elementos em nodes e deixamos relations vazio.
953971
O validator.validate_chain() faz a separacao correta.
954972
"""
973+
meta = tree.meta
974+
items = tree.children
955975
elements: List[str] = []
956976
locations: List[SourceLocation] = []
957977
for item in items:

0 commit comments

Comments
 (0)