Memelang v11
De taal fungeert als een 'low-token' tussenrepresentatie (Intermediate Representation), die vervolgens programmatisch wordt vertaald naar de uitgebreidere SQL-code voor de database.
Voorbeeld van tokenreductie
Memelang (20 tokens): roles actor :$a="Mark Hamill";movie _;@ @ @;actor !$a
SQL (36 tokens): SELECT x.actor,x.movie,y.movie,y.actor FROM roles AS x,roles AS y WHERE x.actor='Mark Hamill' AND y.id!=x.id AND y.movie=x.movie
---
Technische Specificaties en Syntaxis
Memelang is ontworpen als een beknopte query DSL IR voor LLM text-to-SQL conversies.
Grammatica
De taal maakt gebruik van een axiale grammatica met de volgende hiërarchie: Axis2 → Axis1 → Axis0 → Cell
Hierbij zijn spaties syntactisch relevant en triggeren zij een "nieuwe Cell". Er mag geen spatie staan tussen operatoren, comparatoren, komma's of vlaggen en hun respectievelijke waarden.
Basis-syntaxis
De basisstructuur van de taal is als volgt gedefinieerd: [tabel WS] [kolom WS] [":$" variabele][":" ("min"|"max"|"cnt"|"sum"|"avg"|"last"|"grp")] [":" ("asc"|"des")] ["<=>" "\"" string "\""] [("="|"!="|">"|"<"|">="|"<="|"~"|"!~") (string|int|float|("$" variabele)|"@"|"_")] ";"
---
Gebruiksvoorbeelden
Hieronder volgen diverse scenario's waarin Memelang wordt toegepast om database-informatie op te vragen.
Eenvoudige selecties
- Alle films:
movies ;; - Alle rollen:
roles ;; - Titels en beschrijvingen van films:
movies title ; description ;; - Namen en leeftijden van acteurs:
actors name ; age ;;
Filtering en condities
- Acteurs van 41 jaar of ouder:
actors age >=41; _;; - Specifieke rollen (ID 567 en 8901):
roles id 567,8901; _;; - Films met dystopische narratieven (sim > 0.33):
movies description <=>"dystopian"<0.33; _;; - Films met "Star" in de titel uit 1977 of 1980:
movies title ~"Star"; year 1977,1980; _;; - Acteurs met naam die lijkt op "Ana", leeftijd tussen 20 en 35 jaar:
actors name ~"Ana"; age >=20;<=35; _;;
Complexe queries en joins
- Rollen met een score lager dan 1.5 voor films van vóór 1980:
movies year <1980; title ; roles movie @; rating <1.5; ;;
- Sortering op rating (dalend) en film (dalend):
roles rating :des; movie :des;;
- Alle films van vóór 1970, gesorteerd op jaar (oplopend):
movies year :asc<1970; _;;
- Gemiddelde performer-rating van minimaal 4.2:
roles rating :avg>=4.2; actor :grp;;
- Minimale rolbeoordeling per acteur, van laag naar hoog:
roles rating :min:asc; actor :grp;;
- Rollen in films waar "robot" in de beschrijving voorkomt en een rating van 3+ heeft:
movies description <=>"robot"<=$sim; title _; roles movie @; rating >=3;;
- Costars die gezien zijn met Bruce Willis of Uma Thurman:
roles actor :$a~"Bruce Willis","Uma Thurman"; movie _;@ @ @; actor !$a;;
Geavanceerde filters en limieten
- Oorlogsverhalen van vóór 1980: de top 12 films op basis van minimale rolbeoordeling:
movies year <1980; description <=>"war"<=$sim; title :grp; roles movie @; rating :min:des;%beg=0;%lim=12;;
- Rollen voor films "Hero" of "House of Flying Daggers" waar de acteur naam "Li" bevat, gesorteerd van A-Z:
movies title "Hero","House of Flying Daggers"; roles movie @; actor :asc~"Li";;
- Titels die "Here" bevatten over robots tussen 1900 en 2000:
movies title ~"Hero"; description <=>"robot"; year >=1900; <=2000;;
---
Referentie-implementatie (Python)
De onderstaande code vormt de basis voor de parsing en vertaling van Memelang naar SQL.
import re, sys, json
from typing import Optional, Union, List, Iterator, Pattern, Any
Err = SyntaxError
### SYNTAX ###
CELL_PATTERN = (
('QUO', r'"(?:[^"\\\n\r]|\\.)*"'),
('EMB', r'\[(?:-?\d+(?:\.\d+)?)(?:\s*,\s*-?\d+(?:\.\d+)?)*\]'),
('MOD', r'<->|<=>|<#>'),
('CMP', r'>=|<=|!~|!=|=|>|<|~|!'),
('BIND', r':\$\w+'),
('FLAG', r':[a-zA-Z]+'),
('VAR', r'\$\w+'),
('REL', r'@\d?|\^'),
('WLD', r'_'),
('EVAR', r'%[a-zA-Z0-9_]+'),
('SLOT', r'#%?[a-zA-Z0-9_]+'),
('ASSN', r':#[a-zA-Z0-9_]+'),
('TIM', r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'),
('DEC', r'-?\d*\.\d+'),
('INT', r'-?\d+'),
('ALN', r'[A-Za-z][A-Za-z0-9_]*'),
('OR', r','),
('WS', r'\s+'),
('MISMATCH', r'.'),
)
CANON = {'!':'!='}
CELL_REGEX=re.compile("|".join(f"(?P<{k}>{p})" for k, p in CELL_PATTERN))
PAD_MODES = {'qry','tab'}
FLAG_KINDS = {'FLAG','BIND','EVAR','ASSN'}
LIT_KINDS = {'TIM','DEC','INT','ALN','QUO','EMB'}
VAR_KINDS = {'VAR','WLD','REL','EVAR','SLOT'}
DAT_KINDS = LIT_KINDS | VAR_KINDS
RELCOORD = {
'@0': ['-1','-1'],
'@1': ['-1','-2'],
'@2': ['-1','-3'],
'@3': ['-1','-4'],
'@4': ['-1','-5'],
'@' : ['-1','+0'],
'^' : ['-1','end','+0'],
}
class Tok:
def __init__(self, kind: str, src: str, canon: Optional[str] = None):
self.kind = kind
self.src = src
canon = src if canon is None else canon
self.canon = CANON.get(canon) or canon
parser = {'QUO': json.loads, 'EMB': json.loads, 'DEC': float, 'INT': int}.get(kind)
self.dat = parser(src) if parser else src
def __str__(self): return self.src
def __repr__(self): return self.canon
def __eq__(self, other): return repr(self) == repr(other)
def __hash__(self): return hash(self.src)
def __bool__(self): return bool(self.src)
TOK_NULL = Tok('NULL', '')
class Seq(list[Tok]):
opr: Tok = TOK_NULL
def __init__(self, *items):
super().__init__(items)
self.opr = TOK_NULL
def __str__(self): return self.opr.src.join([str(t) for t in self if len(str(t)) or t.kind=='HOLD'])
def __repr__(self): return self.opr.src.join([repr(t) for t in self])
class Cell:
flag: Seq
left: Seq
comp: Tok
right: Seq
padded = False
def __init__(self, src: str):
self.left = Seq()
self.flag = Seq()
self.comp = Tok('EQL', '', '=')
self.right = Seq(Tok('WLD', '', '_'))
toks = []
for m in CELL_REGEX.finditer(src):
kind = m.lastgroup
text = m.group()
if kind == 'WS': continue
if kind == 'MISMATCH': raise Err(f'E_TOK {text!r}')
toks.append(Tok(kind, text))
i, n = 0, len(toks)
def peek(): return toks[i].kind if i < n else ''
def take():
nonlocal i
if i >= n: raise Err('E_EOF')
t = toks[i]
i += 1
return t
while peek() in FLAG_KINDS:
self.flag.append(take())
if peek() == 'MOD':
self.left.opr = take()
self.left.append(Tok('HOLD', ''))
t = take()
if not t.kind in DAT_KINDS: raise Err('E_TERM_DAT')
self.left.append(t)
if peek() == 'CMP':
self.comp = take()
if not peek() in DAT_KINDS: raise Err('E_DAT')
if peek() in DAT_KINDS:
self.right.clear()
while peek() in DAT_KINDS:
self.right.append(take())
if peek() == 'OR':
self.right.opr = take()
if not peek() in DAT_KINDS: raise Err('E_OR_TRAIL')
if i != n: raise Err(f'E_EXPR_TRAIL {toks[i:]}')
def vectorize(self, tok: Tok) -> Tok:
if tok.kind == 'EMB': return tok
if tok.kind not in {'QUO', 'ALN'}: raise Err('E_EMBED')
return Tok('EMB', json.dumps([0.1, 0.2]))
@property
def single(self) -> Tok:
return self.right[0] if self.comp.canon == '=' and len(self.right) == 1 else TOK_NULL
@property
def literal(self) -> Tok:
tok = self.single
return tok if tok.kind in LIT_KINDS else TOK_NULL
def find(self, kind:str) -> Tok:
return next((flag for flag in self.flag if flag.kind == kind), TOK_NULL)
def bind(self, tok: Tok):
if tok not in self.flag: self.flag.append(tok)
def __str__(self) -> str: return f"{self.flag}{self.left}{self.comp}{self.right}"
def __repr__(self) -> str: return f"{self.flag!r}{self.left!r}{self.comp!r}{self.right!r}"
def __bool__(self) -> bool: return bool(self.flag or self.left or self.right)
class Axis(list):
src: str = ''
sep: str = None
sepreg: str = None
sepstr: str = None
empt: bool = False
sub = None
def __init__(self, src: str):
if self.sep is None: raise Err('E_AXIS_SEP')
if not self.sepreg: self.sepreg = re.escape(self.sep)
if not self.sepstr: self.sepstr = self.sep + ' '
self.src= src
self.parse(src.strip())
@property
def regex(self) -> Pattern[str]:
return re.compile(rf'''
(?P<COMM>"""(?:(?!""")[^\n\r\\]|\\.)*""")|
(?P<EXPQ>"(?:[^"\\\n\r]|\\.)*")|
(?P<SEP>{self.sepreg})|
(?P<EXPM>[^"{re.escape(self.sep[0])}]+)|
(?P<EXPS>.)
''', re.VERBOSE)
def parse(self, src: str):
exprs: List[str] = []
for m in self.regex.finditer(src):
if m.lastgroup == 'SEP':
if exprs or self.empt:
self.append(self.sub("".join(exprs)))
exprs.clear()
elif m.lastgroup != 'COMM': exprs.append(m.group())
if exprs: self.append(self.sub("".join(exprs)))
def pull(self, coords):
value = self
for coord in coords: value = value[coord]
return value
def __str__(self) -> str:
items = [str(t) for t in self]
return self.sepstr.join([s for s in items if (s or self.empt)])
class Axis0(Axis):
sep = ' '
sepstr = ' '
sepreg = r'\s+'
sub = Cell
class Axis1(Axis):
sep = ';'
sub = Axis0
class Axis2(Axis):
sep = ';;'
sub = Axis1
@staticmethod
def coordrel(coords, rel):
rel = ["+0"] * max(0, len(coords) - len(rel)) + [str(op) for op in rel]
out = []
for coord, op in zip(coords, rel):
if op == "end": v=-1
elif (v:=(coord + int(op)))<0: raise ValueError('E_REL_BIND')
out.append(v)
return out
def rect(self):
env = {'mode':'qry'}
slots = [Tok('EVAR','','%tab'), Tok('EVAR','','%col'), Tok('EVAR','','%val')]
for idx2, axis1 in enumerate(self):
idx = [idx2, None, None]
for idx1, axis0 in enumerate(axis1):
if not axis0: continue
idx[1:] = [idx1, None]
if bool(axis0[0].find('EVAR')):
for cell in axis0:
if not bool(cell.find('EVAR')): raise Err('E_AXIS_MET')
env[cell.find('EVAR').canon[1:]] = cell.single.dat
continue
if axis0[0].single.kind=='SLOT':
slots=[]
for cell in axis0:
if cell.single.kind!='SLOT': raise Err('E_AXIS_SLOT')
if cell.single.canon[1]=='%': slots.append(Tok('EVAR', '', cell.single.canon[1:]))
else: slots.append(Tok('ASSN','',':'+cell.single.canon))
continue
if env['mode'] not in PAD_MODES: continue
axis0len=len(slots)
if len(axis0) > axis0len: raise Err('E_AXIS0_LONG')
for _ in range(axis0len - len(axis0)):
cell = axis0.sub('@')
cell.padded=True
axis0.insert(0, cell)
for idx0, cell in enumerate(axis0):
idx[2] = idx0
cell.bind(slots[idx0])
for seq in (cell.left, cell.right):
for n, tok in enumerate(seq):
if tok.kind!='REL': continue
coords=self.coordrel(idx, RELCOORD[tok.canon])
src=self.pull(coords)
if src.literal.kind != 'NULL':
seq[n] = src.literal
continue
name = '$'+'_'.join(map(str, coords)).replace('-1','E')
seq[n] = Tok('VAR', '', name)
src.bind(Tok('BIND', '', ':'+name))
PH = '%s'
Param = List[Union[int, float, str, list]]
class SQL:
def __init__(self, sql: str = '', param: Optional[Param] = None):
self.sql = sql
self.param = [] if param is None else list(param)
def sql_value(self) -> "SQL":
return self
def __str__(self) -> str:
sql = self.sql
for p in self.param: sql = sql.replace(PH, json.dumps(p), 1)
return sql
def __repr__(self) -> str: return str((self.sql, self.param))
@staticmethod
def uniq(terms: "SQL") -> list["SQL"]:
out, seen = [], set()
for term in terms:
if term is None: continue
key = (term.sql, tuple(map(repr, term.param)))
if key in seen: continue
seen.add(key)
out.append(term)
return out
class CellSQL(Cell):
flag2agg = {':cnt':'COUNT', ':sum':'SUM', ':avg':'AVG', ':min':'MIN', ':max':'MAX', ':last':'MAX'}
cmp2sql = {'~':' ILIKE ', '!~':' NOT ILIKE '}
mod2sql = {}
def __init__(self, src: str):
super().__init__(src)
self.base = self.alias = ''
self.param = []
flags = {t.canon for t in self.flag if t.kind == 'FLAG'}
self.agg = next((sql for flag, sql in self.flag2agg.items() if flag in flags), '')
self.grouped = ':grp' in flags
self.sort = 'ASC' if ':asc' in flags else 'DESC' if ':des' in flags else ''
def deref(self, bind: dict[str, SQL], with_agg: bool = True) -> Iterator[SQL]:
for t in self.right:
if t.kind == 'VAR':
key = t.canon[1:]
if key not in bind: raise Err(f'E_VAR_BIND {key}')
ref = bind[key]
if isinstance(ref, CellSQL): yield ref.sql_value(with_agg=with_agg)
else: yield ref.sql_value()
else:
yield SQL(PH, [t.dat])
continue
@property
def sql_groupby(self) -> Optional[SQL]:
if not self.grouped: return None
if self.agg: raise Err('E_GRP_AGG')
return SQL(self.base, self.param)
def sql_value(self, grouped: bool = False, alias: bool = False, order: bool = False, with_agg: bool = True) -> SQL:
sql, param = self.base, list(self.param)
if self.left.opr.kind == 'MOD':
sql = f'({sql}{self.left.opr.canon}{PH}::VECTOR)'
param.append(self.vectorize(self.left[1]).canon)
agg = self.agg or ('MAX' if grouped and not self.grouped else '') if with_agg else ''
if agg: sql = f'{agg}({sql})'
if alias and self.alias: sql = f'{sql} AS {self.alias}'
if order and self.sort: sql = f'{sql} {self.sort}'
return SQL(sql, param)
def sql_clause(self, bind: dict[str, SQL]) -> Optional[tuple[str, SQL]]:
if not self.right or self.single.canon == '_': return None
left = self.sql_value()
rights = list(self.deref(bind, with_agg=bool(self.agg)))
comp = self.comp.canon
sqlcomp = self.cmp2sql.get(self.comp.canon) or self.comp.canon
if comp in {'>', '<', '>=', '<='} and len(rights) != 1: raise Err('E_COMP_OR')
items, params = [], []
for right in rights:
items.append(f"CONCAT('%', {right.sql}, '%')" if comp in {'~', '!~'} else right.sql)
params.extend(right.param)
if len(items) == 1: beg, end = '', ''
elif comp in {'=', '~'}: beg, end = 'ANY(ARRAY[', '])'
elif comp in {'!=', '!~'}: beg, end = 'ALL(ARRAY[', '])'
else: raise Err('E_COMP_OR2')
return ('having' if self.agg else 'where'), SQL(f"{left.sql}{sqlcomp}{beg}{','.join(items)}{end}", left.param + params)
class Grid(Axis2):
def select(self) -> List[SQL]:
self.rect()
out = []
env = {'mode':'qry', 'sim':0.5,'tab':'','taba':'','cola':''}
for axis1 in self:
env['lim'], env['beg'] = 0, 0
bind = {k: SQL(PH, [v]) for k, v in env.items()}
tab_cnt = 0
qry = {'select':[], 'from':[], 'fromall':[], 'groupby':[], 'where':[], 'having':[], 'orderby':[]}
grouped = False
allselected = False
for axis0 in axis1:
if env['mode']!='qry': continue
if axis0.src == '_':
allselected = True
continue
for idx0, cell in enumerate(axis0):
single = cell.single.dat
if cell.padded or cell.single.kind=='SLOT': continue
evarval = cell.find('EVAR').canon
if evarval=='%val': pass
elif evarval:
env[evarval[1:]] = cell.single.dat
bind[evarval[1:]]=SQL(PH, [cell.single.dat])
if evarval=='%tab':
if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_$]{0,62}', single): raise Err('E_TAB_NAME')
tab_cnt += 1
env['tab']=single
env['taba']=f"t{tab_cnt}"
qry['from'].append(SQL(f"{env['tab']} AS {env['taba']}"))
qry['fromall'].append(env['taba'])
elif evarval=='%col':
if single == '_': allselected = True
elif not re.fullmatch(r'[A-Za-z_]+[A-Za-z0-9_$]{0,62}', single): raise Err('E_COL_NAME')
env['cola'] = single
continue
assnval = cell.find('ASSN').canon
if assnval: env['cola'] = assnval[2:]
if not env['taba']: raise Err('E_TAB_REQ')
valcell = CellSQL(repr(cell))
valcell.base = f"{env['taba']}.{env['cola']}"
qry['select'].append(valcell)
if valcell.grouped:
grouped = True
qry['groupby'].append(valcell)
if valcell.sort: qry['orderby'].append(valcell)
clause = valcell.sql_clause(bind)
if clause:
key, term = clause
qry[key].append(term)
for flag in valcell.flag:
if flag.kind != 'BIND': continue
if flag.canon[2:] in env: raise Err('E_ENV_BIND')
bind[flag.canon[2:]] = valcell
if not qry['from']:
out.append(SQL())
continue
parts = (
('SELECT', ', ', [SQL(f"{a}.*") for a in qry['fromall']] if allselected else SQL.uniq(t.sql_value(grouped, True) for t in qry['select'])),
('FROM', ', ', qry['from']),
('WHERE', ' AND ', qry['where']),
('GROUP BY', ', ', SQL.uniq(t.sql_groupby for t in qry['groupby'])),
('HAVING', ' AND ', qry['having']),
('ORDER BY', ', ', SQL.uniq(t.sql_value(grouped, False, True) for t in qry['orderby'])),
)
sql, param = [], []
for keyword, sep, terms in parts:
if not terms: continue
sql.append(f"{keyword} " + sep.join(t.sql for t in terms))
for t in terms: param.extend(t.param)
if env['lim']: sql.append(f"LIMIT {int(env['lim'])}")
if env['beg']: sql.append(f"OFFSET {int(env['beg'])}")
out.append(SQL(' '.join(sql), param))
return out
---
Licentie en Juridische Informatie
Deze software is gratis te gebruiken voor ontwikkelings-, test- en educatieve doeleinden. Commerciële implementatie, redistributie of gebruik in productie vereist een aparte licentie.
©2026 HOLTWORK LLC U.S. Pat. 12,475,098 Contact: info@memelang.net
Groetjes,