atlispcc 0.1.0__py3-none-any.whl
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.
- atlispcc-0.1.0.dist-info/METADATA +10 -0
- atlispcc-0.1.0.dist-info/RECORD +25 -0
- atlispcc-0.1.0.dist-info/WHEEL +5 -0
- atlispcc-0.1.0.dist-info/entry_points.txt +2 -0
- atlispcc-0.1.0.dist-info/top_level.txt +4 -0
- compiler/__init__.py +0 -0
- compiler/analyzer.py +217 -0
- compiler/assembler.py +265 -0
- compiler/ast.py +120 -0
- compiler/cad_symbols.py +145 -0
- compiler/cli.py +434 -0
- compiler/codegen.py +767 -0
- compiler/fas_writer.py +123 -0
- compiler/lexer.py +102 -0
- compiler/linker.py +468 -0
- compiler/opcodes.py +134 -0
- compiler/opt.py +1376 -0
- compiler/packages.py +49 -0
- compiler/parser.py +275 -0
- compiler/resource_builder.py +263 -0
- compiler/symbol_table.py +217 -0
- compiler/vlx_builder.py +92 -0
- fas4_decompiler.py +2722 -0
- fas_core.py +699 -0
- validate.py +240 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atlispcc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FAS4 bytecode decompiler and compiler toolchain for AutoLISP-compatible CAD platforms
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Requires-Dist: typing-extensions>=4.5.0
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
9
|
+
Requires-Dist: black>=22.3.0; extra == "dev"
|
|
10
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
fas4_decompiler.py,sha256=Z1lguVxy1KuPJx9Rcz-MI6tyD2YC852oeFD9vz-MhD0,113187
|
|
2
|
+
fas_core.py,sha256=FhXKyO_HMmyZNuM-EVGCDqoXKQGycRkVHLyzFZFftXo,23976
|
|
3
|
+
validate.py,sha256=7G6Y4G8BranlICusBlmaNR5lyx3GofEK1QJT0pcvoa8,7669
|
|
4
|
+
compiler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
compiler/analyzer.py,sha256=ahxB5N3rU8t78LCBVp8kWq_QQGIK4Lu_dotv3ulRr-o,6508
|
|
6
|
+
compiler/assembler.py,sha256=IS0HL2F__ZNox40JcXO7Ori6PINlhz4cy5nxFe2rhXQ,9332
|
|
7
|
+
compiler/ast.py,sha256=isnMGZRSYBnumoHGFU-n00hmNb6fHhaFYx8Rx2FpivY,2217
|
|
8
|
+
compiler/cad_symbols.py,sha256=rqA6C6cBuXrIxV1w_cYjc5u-7-ELaKKvNyIfD34KYv8,5678
|
|
9
|
+
compiler/cli.py,sha256=DNRY-0jR9tQY8hZ5p2rQUhTF0Fl_3h2RbxmCjGydte0,17881
|
|
10
|
+
compiler/codegen.py,sha256=8z8PtoZcrZlXKt6d-AQICrxQXIQz7g6TNpHeC7V2rm0,29140
|
|
11
|
+
compiler/fas_writer.py,sha256=qQx6JwEBsOZ8owIpeoltmqePZjhQvwMIEGlSfYhIr1Q,4004
|
|
12
|
+
compiler/lexer.py,sha256=BsW4vafVCf9dog1nMVpfBxjgbC5P_PRaIbIN1WpNV-k,2704
|
|
13
|
+
compiler/linker.py,sha256=TlD7N0AuKy-HuaqSJJvr-Z2QkpZyV5Sm0BSSd0HzaJ0,20538
|
|
14
|
+
compiler/opcodes.py,sha256=_4xOnuuMQBIR5ghR6xTWLvjD0Wna21QqQkvGWDl82mU,5375
|
|
15
|
+
compiler/opt.py,sha256=TvN6J7Mw-GjANX2GbiGsePcut8iZDCb5hDFy_mpdBhY,50300
|
|
16
|
+
compiler/packages.py,sha256=TZKa2dxtzS-Q4t2elCGSdG_s8c4bem7HbMpK2g0xg-E,1440
|
|
17
|
+
compiler/parser.py,sha256=pfoII5WSlrGAW7Bu4L03fNsFrfgZEqDUHsZfrJc3Z2g,11452
|
|
18
|
+
compiler/resource_builder.py,sha256=VADirA_pRFcRQlPi4k7mufNv00UbCFtOOPO4Ig7EKY4,10873
|
|
19
|
+
compiler/symbol_table.py,sha256=tFPqLiLn6VRe-A3D8bS5-x4rh4JZKA37bqDwrEDFefE,8869
|
|
20
|
+
compiler/vlx_builder.py,sha256=LBH_bYRl4r4-9KPWpB2RkBIL2QMlvumshQvqE5DVHI0,2917
|
|
21
|
+
atlispcc-0.1.0.dist-info/METADATA,sha256=fvTsAPGnSAO0DXJpf4zvojjY-CIS9CanNyj0lML4BL4,363
|
|
22
|
+
atlispcc-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
23
|
+
atlispcc-0.1.0.dist-info/entry_points.txt,sha256=3VWSfOrIUal05XiRUYbqHvP5Lzb8tCMjEj2ngYnxv5E,47
|
|
24
|
+
atlispcc-0.1.0.dist-info/top_level.txt,sha256=tQ25b7vfMG8b2mIVPwdmSi9jAPbwfjwi-hvgPdJDyuI,43
|
|
25
|
+
atlispcc-0.1.0.dist-info/RECORD,,
|
compiler/__init__.py
ADDED
|
File without changes
|
compiler/analyzer.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from typing import Dict, List, Set, Tuple, Optional
|
|
2
|
+
from compiler.ast import (
|
|
3
|
+
Node,
|
|
4
|
+
Program,
|
|
5
|
+
Symbol,
|
|
6
|
+
Integer,
|
|
7
|
+
Real,
|
|
8
|
+
String,
|
|
9
|
+
ListExpr,
|
|
10
|
+
QuotedExpr,
|
|
11
|
+
Defun,
|
|
12
|
+
Setq,
|
|
13
|
+
IfNode,
|
|
14
|
+
WhileNode,
|
|
15
|
+
ForeachNode,
|
|
16
|
+
RepeatNode,
|
|
17
|
+
Call,
|
|
18
|
+
Progn,
|
|
19
|
+
LambdaNode,
|
|
20
|
+
Cond,
|
|
21
|
+
)
|
|
22
|
+
from compiler.codegen import CodeGenerator, BytecodeBuffer
|
|
23
|
+
from compiler.symbol_table import SymbolTable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def count_instructions(func: Defun, symtab: SymbolTable) -> int:
|
|
27
|
+
codegen = CodeGenerator(symtab)
|
|
28
|
+
bytecode, _ = codegen.generate_function(func)
|
|
29
|
+
return len(bytecode)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _collect_call_names(node: Optional[Node], calls: Set[str]):
|
|
33
|
+
if node is None:
|
|
34
|
+
return
|
|
35
|
+
if isinstance(node, Call):
|
|
36
|
+
calls.add(node.name)
|
|
37
|
+
for a in node.args:
|
|
38
|
+
_collect_call_names(a, calls)
|
|
39
|
+
elif isinstance(node, Setq):
|
|
40
|
+
for _, v in node.pairs:
|
|
41
|
+
_collect_call_names(v, calls)
|
|
42
|
+
elif isinstance(node, IfNode):
|
|
43
|
+
_collect_call_names(node.cond, calls)
|
|
44
|
+
if node.then:
|
|
45
|
+
_collect_call_names(node.then, calls)
|
|
46
|
+
if node.else_expr:
|
|
47
|
+
_collect_call_names(node.else_expr, calls)
|
|
48
|
+
elif isinstance(node, WhileNode):
|
|
49
|
+
_collect_call_names(node.cond, calls)
|
|
50
|
+
for b in node.body:
|
|
51
|
+
_collect_call_names(b, calls)
|
|
52
|
+
elif isinstance(node, ForeachNode):
|
|
53
|
+
_collect_call_names(node.var, calls)
|
|
54
|
+
_collect_call_names(node.list_expr, calls)
|
|
55
|
+
for b in node.body:
|
|
56
|
+
_collect_call_names(b, calls)
|
|
57
|
+
elif isinstance(node, RepeatNode):
|
|
58
|
+
_collect_call_names(node.count, calls)
|
|
59
|
+
for b in node.body:
|
|
60
|
+
_collect_call_names(b, calls)
|
|
61
|
+
elif isinstance(node, Progn):
|
|
62
|
+
for f in node.forms:
|
|
63
|
+
_collect_call_names(f, calls)
|
|
64
|
+
elif isinstance(node, Cond):
|
|
65
|
+
for cl in node.clauses:
|
|
66
|
+
for c in cl:
|
|
67
|
+
_collect_call_names(c, calls)
|
|
68
|
+
elif isinstance(node, LambdaNode):
|
|
69
|
+
for b in node.body:
|
|
70
|
+
_collect_call_names(b, calls)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_tail_call(node: Node, func_name: str) -> bool:
|
|
74
|
+
if isinstance(node, Call) and node.name == func_name:
|
|
75
|
+
return True
|
|
76
|
+
if isinstance(node, IfNode):
|
|
77
|
+
if node.then and _is_tail_call(node.then, func_name):
|
|
78
|
+
return True
|
|
79
|
+
if node.else_expr and _is_tail_call(node.else_expr, func_name):
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
82
|
+
if isinstance(node, Cond):
|
|
83
|
+
for clause in node.clauses:
|
|
84
|
+
if clause:
|
|
85
|
+
tail = clause[-1] if len(clause) > 1 else clause[0]
|
|
86
|
+
if _is_tail_call(tail, func_name):
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
if isinstance(node, Progn):
|
|
90
|
+
if node.forms:
|
|
91
|
+
return _is_tail_call(node.forms[-1], func_name)
|
|
92
|
+
return False
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def find_tail_recursive(func: Defun) -> bool:
|
|
97
|
+
if not func.body:
|
|
98
|
+
return False
|
|
99
|
+
return _is_tail_call(func.body[-1], func.name)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def estimate_constants(program: Program) -> int:
|
|
103
|
+
count = 0
|
|
104
|
+
|
|
105
|
+
def walk(node: Optional[Node]):
|
|
106
|
+
nonlocal count
|
|
107
|
+
if node is None:
|
|
108
|
+
return
|
|
109
|
+
if isinstance(node, (Integer, Real, String)):
|
|
110
|
+
count += 1
|
|
111
|
+
elif isinstance(node, Call):
|
|
112
|
+
for a in node.args:
|
|
113
|
+
walk(a)
|
|
114
|
+
elif isinstance(node, Setq):
|
|
115
|
+
for _, v in node.pairs:
|
|
116
|
+
walk(v)
|
|
117
|
+
elif isinstance(node, IfNode):
|
|
118
|
+
walk(node.cond)
|
|
119
|
+
if node.then:
|
|
120
|
+
walk(node.then)
|
|
121
|
+
if node.else_expr:
|
|
122
|
+
walk(node.else_expr)
|
|
123
|
+
elif isinstance(node, WhileNode):
|
|
124
|
+
walk(node.cond)
|
|
125
|
+
for b in node.body:
|
|
126
|
+
walk(b)
|
|
127
|
+
elif isinstance(node, ForeachNode):
|
|
128
|
+
walk(node.var)
|
|
129
|
+
walk(node.list_expr)
|
|
130
|
+
for b in node.body:
|
|
131
|
+
walk(b)
|
|
132
|
+
elif isinstance(node, RepeatNode):
|
|
133
|
+
walk(node.count)
|
|
134
|
+
for b in node.body:
|
|
135
|
+
walk(b)
|
|
136
|
+
elif isinstance(node, Progn):
|
|
137
|
+
for f in node.forms:
|
|
138
|
+
walk(f)
|
|
139
|
+
elif isinstance(node, Cond):
|
|
140
|
+
for cl in node.clauses:
|
|
141
|
+
for c in cl:
|
|
142
|
+
walk(c)
|
|
143
|
+
elif isinstance(node, ListExpr):
|
|
144
|
+
for i in node.items:
|
|
145
|
+
walk(i)
|
|
146
|
+
elif isinstance(node, QuotedExpr):
|
|
147
|
+
if node.expr:
|
|
148
|
+
walk(node.expr)
|
|
149
|
+
elif isinstance(node, LambdaNode):
|
|
150
|
+
for b in node.body:
|
|
151
|
+
walk(b)
|
|
152
|
+
|
|
153
|
+
for f in program.forms:
|
|
154
|
+
if isinstance(f, Defun):
|
|
155
|
+
for e in f.body:
|
|
156
|
+
walk(e)
|
|
157
|
+
else:
|
|
158
|
+
walk(f)
|
|
159
|
+
return count
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def analyze(program: Program) -> Dict:
|
|
163
|
+
symtab = SymbolTable()
|
|
164
|
+
symtab.collect_symbols(program)
|
|
165
|
+
|
|
166
|
+
defuns = [f for f in program.forms if isinstance(f, Defun)]
|
|
167
|
+
other = [f for f in program.forms if not isinstance(f, Defun)]
|
|
168
|
+
|
|
169
|
+
func_stats = []
|
|
170
|
+
total_instructions = 0
|
|
171
|
+
tail_recursive = []
|
|
172
|
+
|
|
173
|
+
for f in defuns:
|
|
174
|
+
inst_count = count_instructions(f, symtab)
|
|
175
|
+
total_instructions += inst_count
|
|
176
|
+
calls: Set[str] = set()
|
|
177
|
+
for e in f.body:
|
|
178
|
+
_collect_call_names(e, calls)
|
|
179
|
+
|
|
180
|
+
is_tail_rec = find_tail_recursive(f)
|
|
181
|
+
if is_tail_rec:
|
|
182
|
+
tail_recursive.append(f.name)
|
|
183
|
+
|
|
184
|
+
func_stats.append(
|
|
185
|
+
{
|
|
186
|
+
"name": f.name,
|
|
187
|
+
"args": len(f.args),
|
|
188
|
+
"locals": len(f.locals),
|
|
189
|
+
"instructions": inst_count,
|
|
190
|
+
"calls": sorted(calls) if calls else [],
|
|
191
|
+
"tail_recursive": is_tail_rec,
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
all_calls: Set[str] = set()
|
|
196
|
+
for f in defuns:
|
|
197
|
+
for e in f.body:
|
|
198
|
+
_collect_call_names(e, all_calls)
|
|
199
|
+
for e in other:
|
|
200
|
+
_collect_call_names(e, all_calls)
|
|
201
|
+
|
|
202
|
+
builtin_calls = {c for c in all_calls if c[0].islower() and c != "setq"}
|
|
203
|
+
user_funcs = {f.name for f in defuns}
|
|
204
|
+
extern_calls = builtin_calls - user_funcs
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"functions": len(defuns),
|
|
208
|
+
"top_level_forms": len(other),
|
|
209
|
+
"total_instructions": total_instructions,
|
|
210
|
+
"constants_estimate": estimate_constants(program),
|
|
211
|
+
"symbol_table_entries": len(symtab.entries),
|
|
212
|
+
"function_offsets": len(symtab.function_offsets),
|
|
213
|
+
"locals": sum(len(f.locals) for f in defuns),
|
|
214
|
+
"tail_recursive_functions": tail_recursive,
|
|
215
|
+
"functions_detail": func_stats,
|
|
216
|
+
"builtin_calls": sorted(extern_calls),
|
|
217
|
+
}
|
compiler/assembler.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""FAS4 assembler/disassembler: .fas ↔ .lasm (Lisp Assembly)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, List, Dict, Tuple, Optional
|
|
6
|
+
|
|
7
|
+
from compiler.opcodes import (
|
|
8
|
+
NAME_TO_OPCODE,
|
|
9
|
+
OPCODE_TO_NAME,
|
|
10
|
+
encode_instruction,
|
|
11
|
+
encode_op_u16,
|
|
12
|
+
)
|
|
13
|
+
from compiler.symbol_table import SymbolTable
|
|
14
|
+
from compiler.resource_builder import ResourceBuilder
|
|
15
|
+
from compiler.fas_writer import FasWriter
|
|
16
|
+
|
|
17
|
+
# ─── Disassembler (FAS → .lasm text) ─────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
# Opcodes that share a name with another opcode — disambiguate in output
|
|
20
|
+
_DUPLICATE_NAMES = {
|
|
21
|
+
0x5C: "GET_LOCAL_WIDE", # shares name 'GET_LOCAL' with 0x05
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _disasm_op_name(opcode: int) -> str:
|
|
26
|
+
"""Return the assembly mnemonic, disambiguating duplicates."""
|
|
27
|
+
if opcode in _DUPLICATE_NAMES:
|
|
28
|
+
return _DUPLICATE_NAMES[opcode]
|
|
29
|
+
return OPCODE_TO_NAME.get(opcode, f"UNK_{opcode:02x}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_sym(symbols: List[str], idx: int) -> str:
|
|
33
|
+
if 0 <= idx < len(symbols):
|
|
34
|
+
return symbols[idx]
|
|
35
|
+
return f"[{idx}]"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fmt_inst(inst, symbols: List[str]) -> str:
|
|
39
|
+
"""Format one instruction as a .lasm line."""
|
|
40
|
+
prefix = f" 0x{inst.offset:04x}:"
|
|
41
|
+
name = _disasm_op_name(inst.opcode)
|
|
42
|
+
ops = list(inst.operands)
|
|
43
|
+
|
|
44
|
+
# DEFUN packs 4 bytes → gv(u16), offset(u16)
|
|
45
|
+
if name == "DEFUN" and len(ops) >= 4:
|
|
46
|
+
gv = ops[0] | (ops[1] << 8)
|
|
47
|
+
off = ops[2] | (ops[3] << 8)
|
|
48
|
+
sym = _resolve_sym(symbols, gv)
|
|
49
|
+
return f"{prefix} DEFUN {sym}, {off}"
|
|
50
|
+
# FUNC/USUBR: second operand is a symbol index
|
|
51
|
+
if name in ("FUNC", "USUBR") and len(ops) >= 2:
|
|
52
|
+
sym = _resolve_sym(symbols, ops[1])
|
|
53
|
+
rest = [str(ops[0]), sym]
|
|
54
|
+
if len(ops) > 2:
|
|
55
|
+
rest.extend(str(o) for o in ops[2:])
|
|
56
|
+
return f'{prefix} {name} {", ".join(rest)}'
|
|
57
|
+
# PUSH_VALUE/PUSH_G/SETQ_G/PUSH_GLOBAL: first operand is a symbol index
|
|
58
|
+
if name in ("PUSH_VALUE", "PUSH_G", "PUSH_GLOBAL", "SETQ_G") and ops:
|
|
59
|
+
sym_info = _resolve_sym(symbols, ops[0])
|
|
60
|
+
rest = [sym_info]
|
|
61
|
+
rest.extend(str(o) for o in ops[1:3])
|
|
62
|
+
return f'{prefix} {name} {", ".join(rest)}'
|
|
63
|
+
# GET_LOCAL/SET_LOCAL/CLEAR_LOCAL: operand is a local slot index (integer)
|
|
64
|
+
if name in ("GET_LOCAL", "GET_LOCAL_WIDE", "SET_LOCAL", "CLEAR_LOCAL") and ops:
|
|
65
|
+
return f"{prefix} {name} {ops[0]}"
|
|
66
|
+
|
|
67
|
+
if not ops:
|
|
68
|
+
return f"{prefix} {name}"
|
|
69
|
+
return f'{prefix} {name} {", ".join(str(o) for o in ops)}'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def disassemble_fas(fas_path: str) -> str:
|
|
73
|
+
"""Disassemble a FAS4 file into .lasm text format."""
|
|
74
|
+
from fas4_decompiler import Fas4Parser, Disassembler, ControlFlowAnalyzer, is_compact_stub, extract_stub_function_names
|
|
75
|
+
|
|
76
|
+
parser = Fas4Parser()
|
|
77
|
+
fas = parser.parse(fas_path)
|
|
78
|
+
disasm = Disassembler(short_subr=is_compact_stub(fas.bytecode, fas.function_catalog, len(fas.symbol_slots)))
|
|
79
|
+
instructions = disasm.disassemble(fas.bytecode, fas.nsyms,
|
|
80
|
+
sym_count=len(fas.symbol_slots) if is_compact_stub(fas.bytecode, fas.function_catalog, len(fas.symbol_slots)) else None)
|
|
81
|
+
cfa = ControlFlowAnalyzer(instructions, fas.function_catalog,
|
|
82
|
+
function_names=extract_stub_function_names(fas))
|
|
83
|
+
functions = cfa.find_functions()
|
|
84
|
+
|
|
85
|
+
lines: List[str] = []
|
|
86
|
+
lines.append("; FAS4 assembly")
|
|
87
|
+
lines.append(f"; Source: {os.path.basename(fas_path)}")
|
|
88
|
+
if fas.crunch_date:
|
|
89
|
+
lines.append(f"; Crunch date: {fas.crunch_date}")
|
|
90
|
+
lines.append("")
|
|
91
|
+
|
|
92
|
+
# 紧凑 stub 格式按资源注册顺序(symbol_slots)编址
|
|
93
|
+
if getattr(fas, "symbol_slots", None):
|
|
94
|
+
symbols = [str(it[1]) for it in fas.symbol_slots]
|
|
95
|
+
else:
|
|
96
|
+
symbols = fas.symbols
|
|
97
|
+
|
|
98
|
+
lines.append(".symbols")
|
|
99
|
+
for idx, sym in enumerate(symbols):
|
|
100
|
+
lines.append(f" {idx}: SYMBOL {sym}")
|
|
101
|
+
lines.append(".end_symbols")
|
|
102
|
+
lines.append("")
|
|
103
|
+
|
|
104
|
+
for func in functions:
|
|
105
|
+
fname = func.name if func.name and func.name != "c:init" else "__init__"
|
|
106
|
+
lines.append(f".function {fname}, {func.sym_idx}")
|
|
107
|
+
for inst in func.instructions:
|
|
108
|
+
lines.append(_fmt_inst(inst, symbols))
|
|
109
|
+
lines.append(".end_function")
|
|
110
|
+
lines.append("")
|
|
111
|
+
|
|
112
|
+
return "\n".join(lines)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ─── Assembler (.lasm text → FAS) ────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def assemble_lasm(lasm_text: str, output_path: str) -> str:
|
|
119
|
+
"""Assemble .lasm text into a FAS4 binary file."""
|
|
120
|
+
|
|
121
|
+
# === Pass 1: parse structure ===
|
|
122
|
+
symbols_list: List[str] = []
|
|
123
|
+
functions: List[Tuple[str, int, List[str]]] = []
|
|
124
|
+
current_func_name = ""
|
|
125
|
+
current_func_idx = 0
|
|
126
|
+
current_func_body: List[str] = []
|
|
127
|
+
in_symbols = False
|
|
128
|
+
in_function = False
|
|
129
|
+
|
|
130
|
+
for raw in lasm_text.split("\n"):
|
|
131
|
+
line = raw.split(";", 1)[0].strip()
|
|
132
|
+
if not line:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if line == ".symbols":
|
|
136
|
+
in_symbols = True
|
|
137
|
+
elif line == ".end_symbols":
|
|
138
|
+
in_symbols = False
|
|
139
|
+
elif line.startswith(".function"):
|
|
140
|
+
m = re.match(r"\.function\s+(\S+),\s*(\d+)", line)
|
|
141
|
+
if m:
|
|
142
|
+
if in_function:
|
|
143
|
+
functions.append((current_func_name, current_func_idx, current_func_body))
|
|
144
|
+
current_func_name = m.group(1)
|
|
145
|
+
current_func_idx = int(m.group(2))
|
|
146
|
+
current_func_body = []
|
|
147
|
+
in_function = True
|
|
148
|
+
elif line == ".end_function":
|
|
149
|
+
if in_function:
|
|
150
|
+
functions.append((current_func_name, current_func_idx, current_func_body))
|
|
151
|
+
current_func_name = ""
|
|
152
|
+
current_func_body = []
|
|
153
|
+
in_function = False
|
|
154
|
+
elif in_symbols:
|
|
155
|
+
m = re.match(r"\s*(\d+)\s*:\s*SYMBOL\s+(.+)", line)
|
|
156
|
+
if m:
|
|
157
|
+
idx = int(m.group(1))
|
|
158
|
+
name = m.group(2).strip()
|
|
159
|
+
while len(symbols_list) <= idx:
|
|
160
|
+
symbols_list.append("")
|
|
161
|
+
symbols_list[idx] = name
|
|
162
|
+
elif in_function:
|
|
163
|
+
current_func_body.append(line)
|
|
164
|
+
|
|
165
|
+
if in_function and current_func_name:
|
|
166
|
+
functions.append((current_func_name, current_func_idx, current_func_body))
|
|
167
|
+
|
|
168
|
+
# === Build SymbolTable with all symbols ===
|
|
169
|
+
symtab = SymbolTable()
|
|
170
|
+
for sym in symbols_list:
|
|
171
|
+
symtab.get_or_add(sym)
|
|
172
|
+
|
|
173
|
+
# === Pass 2: assemble bytecode ===
|
|
174
|
+
init_code = bytearray()
|
|
175
|
+
function_bytes: List[bytes] = []
|
|
176
|
+
function_names: List[str] = []
|
|
177
|
+
|
|
178
|
+
for fname, _fidx, body_lines in functions:
|
|
179
|
+
if fname == "__init__":
|
|
180
|
+
for line in body_lines:
|
|
181
|
+
bc = _asm_instr(line, symtab, symbols_list)
|
|
182
|
+
if bc:
|
|
183
|
+
init_code.extend(bc)
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
fb = bytearray()
|
|
187
|
+
for line in body_lines:
|
|
188
|
+
bc = _asm_instr(line, symtab, symbols_list)
|
|
189
|
+
if bc:
|
|
190
|
+
fb.extend(bc)
|
|
191
|
+
function_bytes.append(bytes(fb))
|
|
192
|
+
function_names.append(fname)
|
|
193
|
+
|
|
194
|
+
# === Build function catalog and write FAS ===
|
|
195
|
+
func_offsets: Dict[str, int] = {}
|
|
196
|
+
all_bc = bytearray(init_code)
|
|
197
|
+
|
|
198
|
+
for fname, fbytes in zip(function_names, function_bytes):
|
|
199
|
+
offset = len(all_bc)
|
|
200
|
+
func_offsets[fname] = offset
|
|
201
|
+
all_bc.extend(fbytes)
|
|
202
|
+
|
|
203
|
+
for name in function_names:
|
|
204
|
+
symtab.add_func_ref(name, func_offsets[name])
|
|
205
|
+
|
|
206
|
+
seg0 = bytes(all_bc)
|
|
207
|
+
resource = ResourceBuilder(symtab)
|
|
208
|
+
seg1 = resource.build(func_offsets)
|
|
209
|
+
writer = FasWriter(symtab)
|
|
210
|
+
writer.write(output_path, seg0, seg1)
|
|
211
|
+
return output_path
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
_INSTR_RE = re.compile(r"(\w+)(.*)")
|
|
215
|
+
_ADDR_PREFIX = re.compile(r"^0x[0-9a-fA-F]+:")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _asm_instr(line: str, symtab: SymbolTable, symbols: List[str]) -> Optional[bytes]:
|
|
219
|
+
"""Parse one assembly instruction line and encode to bytes."""
|
|
220
|
+
content = _ADDR_PREFIX.sub("", line).strip()
|
|
221
|
+
if not content:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
m = _INSTR_RE.match(content)
|
|
225
|
+
if not m:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
mnemonic = m.group(1).upper()
|
|
229
|
+
rest = m.group(2).strip()
|
|
230
|
+
operands = _parse_ops(rest, symtab)
|
|
231
|
+
|
|
232
|
+
if mnemonic == "GET_LOCAL_WIDE":
|
|
233
|
+
# Use wide GET_LOCAL opcode (0x5c) instead of standard (0x05)
|
|
234
|
+
from compiler.opcodes import encode_op_u16
|
|
235
|
+
|
|
236
|
+
op = NAME_TO_OPCODE.get("GET_LOCAL", 0xFF)
|
|
237
|
+
# Force opcode 0x5c
|
|
238
|
+
return encode_op_u16(0x5C, int(operands[0]) if operands else 0)
|
|
239
|
+
|
|
240
|
+
return encode_instruction(mnemonic, operands)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _parse_ops(text: str, symtab: SymbolTable) -> List:
|
|
244
|
+
"""Parse comma-separated operands, resolving symbol names to indices."""
|
|
245
|
+
if not text.strip():
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
parts = [p.strip() for p in text.split(",")]
|
|
249
|
+
result: List[Any] = []
|
|
250
|
+
for p in parts:
|
|
251
|
+
if not p:
|
|
252
|
+
continue
|
|
253
|
+
if p in symtab.name_to_idx:
|
|
254
|
+
result.append(symtab.name_to_idx[p])
|
|
255
|
+
elif p.startswith("[") and p.endswith("]") and p[1:-1].isdigit():
|
|
256
|
+
result.append(int(p[1:-1]))
|
|
257
|
+
else:
|
|
258
|
+
try:
|
|
259
|
+
result.append(int(p))
|
|
260
|
+
except ValueError:
|
|
261
|
+
try:
|
|
262
|
+
result.append(float(p))
|
|
263
|
+
except ValueError:
|
|
264
|
+
raise ValueError(f"Cannot resolve operand: {p!r}")
|
|
265
|
+
return result
|
compiler/ast.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Any, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Node:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Program(Node):
|
|
12
|
+
forms: List['Node'] = field(default_factory=list)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Symbol(Node):
|
|
17
|
+
name: str = ''
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Integer(Node):
|
|
22
|
+
value: int = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Real(Node):
|
|
27
|
+
value: float = 0.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class String(Node):
|
|
32
|
+
value: str = ''
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ListExpr(Node):
|
|
37
|
+
items: List['Node'] = field(default_factory=list)
|
|
38
|
+
dotted: Optional['Node'] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class QuotedExpr(Node):
|
|
43
|
+
expr: Optional['Node'] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Defun(Node):
|
|
48
|
+
name: str = ''
|
|
49
|
+
args: List[str] = field(default_factory=list)
|
|
50
|
+
optional: List[str] = field(default_factory=list)
|
|
51
|
+
rest: Optional[str] = None
|
|
52
|
+
locals: List[str] = field(default_factory=list)
|
|
53
|
+
body: List['Node'] = field(default_factory=list)
|
|
54
|
+
is_command: bool = False
|
|
55
|
+
quoted: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Setq(Node):
|
|
60
|
+
pairs: List[tuple] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class IfNode(Node):
|
|
65
|
+
cond: Optional['Node'] = None
|
|
66
|
+
then: Optional['Node'] = None
|
|
67
|
+
else_expr: Optional['Node'] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class WhileNode(Node):
|
|
72
|
+
cond: Optional['Node'] = None
|
|
73
|
+
body: List['Node'] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ForeachNode(Node):
|
|
78
|
+
var: Optional['Node'] = None
|
|
79
|
+
list_expr: Optional['Node'] = None
|
|
80
|
+
body: List['Node'] = field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class Call(Node):
|
|
85
|
+
name: str = ''
|
|
86
|
+
args: List['Node'] = field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Progn(Node):
|
|
91
|
+
forms: List['Node'] = field(default_factory=list)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class LambdaNode(Node):
|
|
96
|
+
args: List[str] = field(default_factory=list)
|
|
97
|
+
body: List['Node'] = field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class RepeatNode(Node):
|
|
102
|
+
count: Optional['Node'] = None
|
|
103
|
+
body: List['Node'] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Cond(Node):
|
|
108
|
+
clauses: List[tuple] = field(default_factory=list)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class Defpackage(Node):
|
|
113
|
+
name: str = ''
|
|
114
|
+
use: List[str] = field(default_factory=list)
|
|
115
|
+
export: List[str] = field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class InPackage(Node):
|
|
120
|
+
name: str = ''
|