Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Pre-releases use a SemVer suffix (`-dev`). See [`docs/releasing.md`](docs/releas
### Added

- CLI `--dump-plan` / `-d FILE` writes the OPA plan IR JSON before codegen.
- Generate-time error when CallStmt names a builtin the runtime does not implement.

### Fixed

Expand Down
62 changes: 40 additions & 22 deletions docs/ir2lua-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ Bootstrap order (same as `./go`):
| 5 | `membership.t` | `ScanStmt` / `in` |
| 6 | `cmp_*.t` | compare stmts |

Until the binary exists, `t::Rego` still falls back to `--- ref_lua`.
The harness uses `./rego2lua` when present and falls back to `--- ref_lua` otherwise.

---

Expand Down Expand Up @@ -554,26 +554,42 @@ Later slices and runtime-vs-contract notes: [`runtime/README.md`](../runtime/REA

---

## 9. Suggested Python layout
## 9. Python layout

Repo-root scripts plus the `ir2lua` package. There is no `load.py` dataclass layer, no `runtime_lua.py` blob, and no `ir2lua.cli` — the walker uses plan JSON dicts, and helpers live in [`runtime/`](../runtime/).

```text
rego2lua/ # or ir2lua/
__init__.py
load.py # json → Policy dataclasses
operands.py # operand → Lua expr
stmts/ # parse.py dispatcher; one module per stmt family
translate.py # recursive driver
runtime_lua.py # string blob of helpers
cli.py # plan.json → out.lua
VERSION # public version (same as ir2lua.__version__)
rego2lua # CLI: policy.rego → Lua on stdout
opa_plan.py # opa build -t plan → (plan dict, package)
ir2lua/
__init__.py # __version__
operands.py # operand → Lua expr; TranslateError
translate.py # funcs → blocks → stmts → module string
stmts/
parse.py # IR type → handler
ctx.py # Emit / Lowered protocols
assign.py # ResetLocal, AssignVar, AssignVarOnce
defined.py # IsDefined, IsUndefined
dot.py # DotStmt
equal.py # Equal, NotEqual
call.py # CallStmt (planned funcs + builtin allowlist)
return_local.py # ReturnLocalStmt
make.py # MakeNull / MakeNumber* / MakeArray / ArrayAppend
not_stmt.py # NotStmt
block.py # BlockStmt
scan.py # ScanStmt
runtime/ # LuaJIT helpers (not Python)
rego_rt.lua # facade; generated modules load this
```

CLI sketch (historical). Actual paths: `./rego2lua` (CLI), `opa_plan.py` (Rego→IR), `ir2lua/` (IR→Lua).

```bash
python -m rego2lua compile plan.json -o policy.lua
# later: rego2lua foo.rego -e foo/allow -o foo.lua (shells out to opa)
./rego2lua path/to/policy.rego > policy.lua
./rego2lua -d plan.json path/to/policy.rego > policy.lua
```

`opa_plan.build_ir_plan` takes the package path as the OPA entrypoint (`package foo.bar` → `-e foo/bar`). Codegen contract for what generated Lua may call: [`runtime/README.md`](../runtime/README.md#codegen-contract).

---

## 10. Development tips
Expand Down Expand Up @@ -642,11 +658,13 @@ These ranks are **IR statement order** for codegen (unlock `t/*.t`). They are **

## 14. Checklist

- [x] Minimal runtime (slice **1.1.1**): `runtime/` layers + `t/runtime.t`
- [ ] Generate `plan.json` for `example.rego` / a `sanity` policy
- [ ] Load IR in Python; print plan names and stmt type histogram
- [ ] Hand-trace the §4 plan + func locals once
- [ ] Implement **must** IR statements (codegen) on top of the runtime
- [ ] Emit `package` module API matching product shape
- [ ] Green: first case of `t/sanity.t` via IR path
- [ ] Grow statement coverage until `./go` is IR-backed
- [x] Minimal runtime (slice **1.1.1**): `runtime/` layers + `t/runtime.t`
- [x] Generate `plan.json` (`opa_plan.py` / `./rego2lua -d FILE`)
- [x] Load IR in Python (`ir2lua.translate` walks funcs → blocks → stmts)
- [x] Hand-trace the §4 plan + func locals ([`rego-ir-by-example/01-spine-allow.md`](./rego-ir-by-example/01-spine-allow.md))
- [x] Implement **must** IR statements for compiled funcs (codegen on `runtime/`)
- [x] Emit `package` module API matching product shape (`pkg.rule(input, data)`)
- [x] Green: `t/sanity.t` via IR path
- [ ] Grow statement coverage until `./go` is IR-backed (`t/membership.t`: `in` / `internal.member_2`)

Codegen wraps **funcs** as `pkg.rule(input, data)`, so `MakeObjectStmt` / `ObjectInsertStmt` / `ResultSetAddStmt` (plan result-set packing in §11) are unused.
25 changes: 13 additions & 12 deletions docs/rego-ir-by-example/99-stmt-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,19 @@ Lua sketches in this tree are IR-shaped. Product codegen must follow [`runtime/R

## Must-implement checklist (Lua backend v1)

- [ ] CallStmt (planned funcs + builtins)
- [ ] ReturnLocalStmt / ResultSetAddStmt
- [ ] AssignVar / AssignVarOnce / ResetLocal
- [ ] Dot / Equal / NotEqual
- [ ] IsDefined / IsUndefined
- [ ] MakeObject / ObjectInsert
- [ ] ScanStmt
- [ ] MakeArray / ArrayAppend
- [ ] NotStmt / BlockStmt
- [ ] frames: input/data args, dynamic `L` table

Then: ObjectMerge, sets, With, Len/IsArray, number makes.
- [x] CallStmt (planned funcs + builtins)
- [x] ReturnLocalStmt
- [ ] ResultSetAddStmt (unused: codegen wraps funcs, not plan result sets)
- [x] AssignVar / AssignVarOnce / ResetLocal
- [x] Dot / Equal / NotEqual
- [x] IsDefined / IsUndefined
- [ ] MakeObject / ObjectInsert (unused: same as ResultSetAdd)
- [x] ScanStmt
- [x] MakeArray / ArrayAppend
- [x] NotStmt / BlockStmt
- [x] frames: input/data args, `t0`/`t1` plus `tN` slots

Then: ObjectMerge, sets, With, Len/IsArray. MakeNumberInt / MakeNumberRef / MakeNull are already emitted.

---

Expand Down
31 changes: 30 additions & 1 deletion ir2lua/stmts/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,32 @@
from ir2lua.operands import Operand, TranslateError, lua_string
from ir2lua.stmts.ctx import Emit

# IR names implemented in runtime/builtins.lua. Keep in sync.
IMPLEMENTED = frozenset({
"equal",
"neq",
"gt",
"gte",
"lt",
"lte",
"is_string",
"is_number",
"is_boolean",
"is_null",
"is_array",
"is_object",
"is_set",
"type_name",
"to_number",
"plus",
"minus",
"mul",
"div",
"rem",
"abs",
"numbers.range",
})


@dataclass
class CallStmt:
Expand All @@ -32,11 +58,14 @@ def parse(cls, ctx: Emit, stmt: dict[str, Any]) -> CallStmt:
ctx.resolve_operand(a, f"CallStmt arg {i}")
for i, a in enumerate(raw_args)
]
planned = ctx.planned_func_lua(func)
if planned is None and func not in IMPLEMENTED:
raise TranslateError(f"unsupported builtin: {func}")
return cls(
func,
args,
ctx.decl_local(stmt.get("result"), "CallStmt result"),
ctx.planned_func_lua(func),
planned,
)

def emit(self, ctx: Emit, label: str) -> None:
Expand Down
2 changes: 1 addition & 1 deletion runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Loads only `runtime/rego_rt.lua`. Tests use kernel APIs + `call_builtin` (not `r
|--------------|--------|
| Value / tagging | `value.lua` |
| DotStmt | `dot.lua` |
| New CallStmt type/number/compare op | implement on `priv` in the right layer + register in `builtins.lua` |
| New CallStmt type/number/compare op | implement on `priv` in the right layer, register in `builtins.lua`, add the IR name to `ir2lua/stmts/call.py` `IMPLEMENTED` |
| New IR stmt helper for codegen | install on `rt` in the right layer; document here |

Then add a check in `t/runtime_rt.lua` and run `prove t/runtime.t`.
Expand Down
1 change: 1 addition & 0 deletions runtime/builtins.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
-- rt.builtins[name] — raw 3-valued impl (true | false | UNDEF | value)
-- rt.call_builtin — codegen: local def, v = rt.call_builtin(name, ...)
-- def is a boolean; v is the result only when def.
-- Codegen allowlist: ir2lua/stmts/call.py IMPLEMENTED (keep in sync).

return function(rt, priv)
local reg = {
Expand Down
Loading