diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e44f8..12a8eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/ir2lua-guide.md b/docs/ir2lua-guide.md index 3ac7fcb..37d25a1 100644 --- a/docs/ir2lua-guide.md +++ b/docs/ir2lua-guide.md @@ -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. --- @@ -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 @@ -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. diff --git a/docs/rego-ir-by-example/99-stmt-catalog.md b/docs/rego-ir-by-example/99-stmt-catalog.md index 0762208..6a4f2a8 100644 --- a/docs/rego-ir-by-example/99-stmt-catalog.md +++ b/docs/rego-ir-by-example/99-stmt-catalog.md @@ -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. --- diff --git a/ir2lua/stmts/call.py b/ir2lua/stmts/call.py index ea539ba..dfda237 100644 --- a/ir2lua/stmts/call.py +++ b/ir2lua/stmts/call.py @@ -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: @@ -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: diff --git a/runtime/README.md b/runtime/README.md index c4911f3..8e5689c 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -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`. diff --git a/runtime/builtins.lua b/runtime/builtins.lua index 7379867..a654467 100644 --- a/runtime/builtins.lua +++ b/runtime/builtins.lua @@ -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 = {