diff --git a/.build/justfile b/.build/justfile index d3dca5a6..27c5036e 100644 --- a/.build/justfile +++ b/.build/justfile @@ -52,10 +52,10 @@ golden-path: dune build @echo "2. Running tests..." dune runtest - @echo "3. Testing lexer on hello.as..." - dune exec affinescript -- lex examples/hello.as - @echo "4. Testing parser on ownership.as..." - dune exec affinescript -- parse examples/ownership.as + @echo "3. Testing lexer on hello.affine..." + dune exec affinescript -- lex examples/hello.affine + @echo "4. Testing parser on ownership.affine..." + dune exec affinescript -- parse examples/ownership.affine @echo "=== Golden Path Complete ===" # Prepare a release diff --git a/conformance/invalid/001_unclosed_brace.expected b/conformance/invalid/001_unclosed_brace.expected index 464aa212..a1c5e713 100644 --- a/conformance/invalid/001_unclosed_brace.expected +++ b/conformance/invalid/001_unclosed_brace.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: unexpected end of file or missing '}' error[E0002]: Parse error: unexpected end of input - --> conformance/invalid/001_unclosed_brace.as:5:1 + --> conformance/invalid/001_unclosed_brace.affine:5:1 | 5 | // Missing closing brace | ^ expected '}' diff --git a/conformance/invalid/002_unclosed_string.expected b/conformance/invalid/002_unclosed_string.expected index 27447794..e3a426a0 100644 --- a/conformance/invalid/002_unclosed_string.expected +++ b/conformance/invalid/002_unclosed_string.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: unterminated string literal error[E0001]: Lexical error: unterminated string literal - --> conformance/invalid/002_unclosed_string.as:4:11 + --> conformance/invalid/002_unclosed_string.affine:4:11 | 4 | let s = "hello world | ^ string literal not closed diff --git a/conformance/invalid/003_bad_number.expected b/conformance/invalid/003_bad_number.expected index edd60c1e..115cfe02 100644 --- a/conformance/invalid/003_bad_number.expected +++ b/conformance/invalid/003_bad_number.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: invalid hexadecimal literal error[E0001]: Lexical error: invalid hexadecimal literal - --> conformance/invalid/003_bad_number.as:4:11 + --> conformance/invalid/003_bad_number.affine:4:11 | 4 | let x = 0xGHI; | ^^^^^ invalid hex digit 'G' diff --git a/conformance/invalid/004_unexpected_token.expected b/conformance/invalid/004_unexpected_token.expected index 4ad0d7a4..0639d390 100644 --- a/conformance/invalid/004_unexpected_token.expected +++ b/conformance/invalid/004_unexpected_token.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: expected identifier error[E0002]: Parse error: expected identifier - --> conformance/invalid/004_unexpected_token.as:4:7 + --> conformance/invalid/004_unexpected_token.affine:4:7 | 4 | let = 42; | ^ expected identifier, found '=' diff --git a/conformance/invalid/005_missing_arrow.expected b/conformance/invalid/005_missing_arrow.expected index a2b4ad29..3816ef5f 100644 --- a/conformance/invalid/005_missing_arrow.expected +++ b/conformance/invalid/005_missing_arrow.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: expected '->' error[E0002]: Parse error: expected '->' - --> conformance/invalid/005_missing_arrow.as:3:10 + --> conformance/invalid/005_missing_arrow.affine:3:10 | 3 | fn test() Int { | ^ expected '->', found identifier diff --git a/conformance/invalid/006_bad_operator.expected b/conformance/invalid/006_bad_operator.expected index c2206057..9c3c01c1 100644 --- a/conformance/invalid/006_bad_operator.expected +++ b/conformance/invalid/006_bad_operator.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: unknown operator or unexpected character error[E0001]: Lexical error: unexpected character - --> conformance/invalid/006_bad_operator.as:4:15 + --> conformance/invalid/006_bad_operator.affine:4:15 | 4 | let x = 1 @@ 2; | ^ unexpected '@' diff --git a/conformance/invalid/007_incomplete_effect.expected b/conformance/invalid/007_incomplete_effect.expected index 4f7a8ed0..6300107c 100644 --- a/conformance/invalid/007_incomplete_effect.expected +++ b/conformance/invalid/007_incomplete_effect.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: unexpected end of file or missing '}' error[E0002]: Parse error: unexpected end of input - --> conformance/invalid/007_incomplete_effect.as:4:28 + --> conformance/invalid/007_incomplete_effect.affine:4:28 | 4 | fn missing_return_type(); | ^ expected '}', found end of file diff --git a/conformance/invalid/008_mismatched_parens.expected b/conformance/invalid/008_mismatched_parens.expected index cff905a1..ff914c73 100644 --- a/conformance/invalid/008_mismatched_parens.expected +++ b/conformance/invalid/008_mismatched_parens.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: expected ')' error[E0002]: Parse error: expected ')' - --> conformance/invalid/008_mismatched_parens.as:4:16 + --> conformance/invalid/008_mismatched_parens.affine:4:16 | 4 | let x = (1 + 2; | ^ expected ')', found ';' diff --git a/conformance/invalid/009_reserved_keyword.expected b/conformance/invalid/009_reserved_keyword.expected index b60c4a98..5394d268 100644 --- a/conformance/invalid/009_reserved_keyword.expected +++ b/conformance/invalid/009_reserved_keyword.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: expected identifier, found keyword error[E0002]: Parse error: expected identifier - --> conformance/invalid/009_reserved_keyword.as:4:7 + --> conformance/invalid/009_reserved_keyword.affine:4:7 | 4 | let fn = 42; | ^^ expected identifier, found keyword 'fn' diff --git a/conformance/invalid/010_empty_match.expected b/conformance/invalid/010_empty_match.expected index 5b2fb5e5..9cce3d51 100644 --- a/conformance/invalid/010_empty_match.expected +++ b/conformance/invalid/010_empty_match.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: expected at least one match arm error[E0002]: Parse error: expected pattern - --> conformance/invalid/010_empty_match.as:5:3 + --> conformance/invalid/010_empty_match.affine:5:3 | 5 | } | ^ match expression requires at least one arm diff --git a/conformance/invalid/011_bad_escape.expected b/conformance/invalid/011_bad_escape.expected index 4850bedf..bf1a9a7d 100644 --- a/conformance/invalid/011_bad_escape.expected +++ b/conformance/invalid/011_bad_escape.expected @@ -2,7 +2,7 @@ # Exit code: 1 # Error pattern: invalid escape sequence error[E0001]: Lexical error: invalid escape sequence '\q' - --> conformance/invalid/011_bad_escape.as:4:16 + --> conformance/invalid/011_bad_escape.affine:4:16 | 4 | let s = "bad \q escape"; | ^^ unknown escape sequence diff --git a/docs/guides/frontier-programming-practices/AI.a2ml b/docs/guides/frontier-programming-practices/AI.a2ml index f3f2a4a2..14e07494 100644 --- a/docs/guides/frontier-programming-practices/AI.a2ml +++ b/docs/guides/frontier-programming-practices/AI.a2ml @@ -68,7 +68,7 @@ (compile-target (primary "typed-wasm") (secondary "julia") - (rule "AffineScript is a compiled language. Its semantics live in .as source and compiled output. Do not rewrite .as source in another language as a shortcut for refactoring, modularization, or 'improvement'.")) + (rule "AffineScript is a compiled language. Its semantics live in .affine source and compiled output. Do not rewrite .affine source in another language as a shortcut for refactoring, modularization, or 'improvement'.")) (modularization (rule "When asked to modularize or clean up code, operate within AffineScript using the module system, record decomposition (row polymorphism), and phantom type parameters. Do not create new compiler features to solve user-code modularization problems.")) diff --git a/justfile b/justfile index 05bcb6d9..97ea50d0 100644 --- a/justfile +++ b/justfile @@ -89,9 +89,9 @@ golden-path: @echo "2. Running tests..." dune runtest @echo "3. Lexer smoke test..." - dune exec affinescript -- lex examples/hello.affine 2>/dev/null || dune exec affinescript -- lex examples/hello.as 2>/dev/null || echo "(no example file — skip)" + dune exec affinescript -- lex examples/hello.affine 2>/dev/null || dune exec affinescript -- lex examples/hello.affine 2>/dev/null || echo "(no example file — skip)" @echo "4. Ownership smoke test..." - dune exec affinescript -- parse examples/ownership.affine 2>/dev/null || dune exec affinescript -- parse examples/ownership.as 2>/dev/null || echo "(no ownership example — skip)" + dune exec affinescript -- parse examples/ownership.affine 2>/dev/null || dune exec affinescript -- parse examples/ownership.affine 2>/dev/null || echo "(no ownership example — skip)" @echo "=== Golden Path Complete ===" # Run panic-attack security scan diff --git a/lib/codegen.ml b/lib/codegen.ml index 57145bc5..d3b0ab4a 100644 --- a/lib/codegen.ml +++ b/lib/codegen.ml @@ -385,7 +385,7 @@ let gen_unop (op : unary_op) : instr result = match op with | OpNeg -> Ok I32Sub (* 0 - x *) | OpNot -> Ok I32Eqz (* x == 0 *) - | OpBitNot -> Error (UnsupportedFeature "Bitwise NOT") + | OpBitNot -> Ok I32Xor (* -1 ^ x *) | OpRef -> Error (UnsupportedFeature "OpRef handled in ExprUnary") | OpDeref -> Error (UnsupportedFeature "OpDeref handled in ExprUnary") diff --git a/lib/codegen_gc.ml b/lib/codegen_gc.ml index 861c2409..66d179bb 100644 --- a/lib/codegen_gc.ml +++ b/lib/codegen_gc.ml @@ -334,8 +334,12 @@ let rec gen_gc_expr (ctx : gc_ctx) (expr : expr) : (gc_ctx * gc_instr list) cg_r let* (ctx', code) = gen_gc_expr ctx operand in Ok (ctx', code @ [std Wasm.I32Eqz]) - | ExprUnary (_, operand) -> - gen_gc_expr ctx operand + | ExprUnary (OpBitNot, operand) -> + let* (ctx', code) = gen_gc_expr ctx operand in + Ok (ctx', [push_i32 (-1)] @ code @ [std Wasm.I32Xor]) + + | ExprUnary (op, _operand) -> + Error (UnsupportedFeature (Printf.sprintf "Unary operator %s" (show_unary_op op))) (* ── Function calls ────────────────────────────────────────────── *) diff --git a/lib/opt.ml b/lib/opt.ml index 77377049..5630b7c7 100644 --- a/lib/opt.ml +++ b/lib/opt.ml @@ -26,6 +26,11 @@ let rec fold_constants_expr (expr : expr) : expr = | OpLe -> ExprLit (LitBool (a <= b, Span.dummy)) | OpGt -> ExprLit (LitBool (a > b, Span.dummy)) | OpGe -> ExprLit (LitBool (a >= b, Span.dummy)) + | OpBitAnd -> ExprLit (LitInt (a land b, Span.dummy)) + | OpBitOr -> ExprLit (LitInt (a lor b, Span.dummy)) + | OpBitXor -> ExprLit (LitInt (a lxor b, Span.dummy)) + | OpShl -> ExprLit (LitInt (a lsl b, Span.dummy)) + | OpShr -> ExprLit (LitInt (a lsr b, Span.dummy)) | _ -> expr (* Don't fold other ops or division by zero *) end @@ -45,6 +50,9 @@ let rec fold_constants_expr (expr : expr) : expr = | ExprUnary (OpNot, ExprLit (LitBool (b, _))) -> ExprLit (LitBool (not b, Span.dummy)) + | ExprUnary (OpBitNot, ExprLit (LitInt (n, _))) -> + ExprLit (LitInt (lnot n, Span.dummy)) + | ExprBinary (left, op, right) -> let left' = fold_constants_expr left in let right' = fold_constants_expr right in diff --git a/test/e2e/fixtures/bitwise.affine b/test/e2e/fixtures/bitwise.affine new file mode 100644 index 00000000..f0bbe4bb --- /dev/null +++ b/test/e2e/fixtures/bitwise.affine @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// End-to-end test: bitwise operations +// Tests: parsing, constant folding, WASM codegen, Julia codegen + +fn bit_and(a: Int, b: Int) -> Int = a & b; +fn bit_or(a: Int, b: Int) -> Int = a | b; +fn bit_xor(a: Int, b: Int) -> Int = a ^ b; +fn bit_not(n: Int) -> Int = ~n; +fn bit_shl(a: Int, b: Int) -> Int = a << b; +fn bit_shr(a: Int, b: Int) -> Int = a >> b; + +fn constant_fold_bitwise() -> Int { + (1 & 3) | (4 ^ 6) | (~(-1)) +} diff --git a/test/golden/binary_ops.affine b/test/golden/binary_ops.affine index ef6a0b93..01288b18 100644 --- a/test/golden/binary_ops.affine +++ b/test/golden/binary_ops.affine @@ -6,3 +6,11 @@ fn test_ops() -> Int { fn test_compare() -> Bool { x == y && a < b } + +fn test_bitwise(a: Int, b: Int) -> Int { + (a & b | a ^ b) << 2 >> 1 +} + +fn test_bitwise_not(n: Int) -> Int { + ~n +} diff --git a/test/golden/binary_ops.expected b/test/golden/binary_ops.expected index 61b9f084..c1694ce9 100644 --- a/test/golden/binary_ops.expected +++ b/test/golden/binary_ops.expected @@ -126,6 +126,206 @@ )) ))) }) + }); + (Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = + { Ast.name = "test_bitwise"; + span = + { Span.start_pos = { Span.line = 10; col = 1; offset = 127 }; + end_pos = { Span.line = 10; col = 4; offset = 130 }; + file = "test/golden/binary_ops.affine" } + }; + fd_type_params = []; + fd_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = + { Ast.name = "a"; + span = + { Span.start_pos = { Span.line = 10; col = 16; offset = 142 }; + end_pos = { Span.line = 10; col = 17; offset = 143 }; + file = "test/golden/binary_ops.affine" } + }; + p_ty = + (Ast.TyCon + { Ast.name = "Int"; + span = + { Span.start_pos = + { Span.line = 10; col = 18; offset = 144 }; + end_pos = { Span.line = 10; col = 20; offset = 146 }; + file = "test/golden/binary_ops.affine" } + }) + }; + { Ast.p_quantity = None; p_ownership = None; + p_name = + { Ast.name = "b"; + span = + { Span.start_pos = { Span.line = 10; col = 23; offset = 149 }; + end_pos = { Span.line = 10; col = 25; offset = 151 }; + file = "test/golden/binary_ops.affine" } + }; + p_ty = + (Ast.TyCon + { Ast.name = "Int"; + span = + { Span.start_pos = + { Span.line = 10; col = 26; offset = 152 }; + end_pos = { Span.line = 10; col = 28; offset = 154 }; + file = "test/golden/binary_ops.affine" } + }) + } + ]; + fd_ret_ty = + (Some (Ast.TyCon + { Ast.name = "Int"; + span = + { Span.start_pos = + { Span.line = 10; col = 33; offset = 159 }; + end_pos = { Span.line = 10; col = 36; offset = 162 }; + file = "test/golden/binary_ops.affine" } + })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprBinary ( + (Ast.ExprBinary ( + (Ast.ExprBinary ( + (Ast.ExprBinary ( + (Ast.ExprVar + { Ast.name = "a"; + span = + { Span.start_pos = + { Span.line = 11; col = 3; + offset = 170 }; + end_pos = + { Span.line = 11; col = 4; + offset = 171 }; + file = "test/golden/binary_ops.affine" + } + }), + Ast.OpBitAnd, + (Ast.ExprVar + { Ast.name = "b"; + span = + { Span.start_pos = + { Span.line = 11; col = 6; + offset = 173 }; + end_pos = + { Span.line = 11; col = 8; + offset = 175 }; + file = "test/golden/binary_ops.affine" + } + }) + )), + Ast.OpBitOr, + (Ast.ExprBinary ( + (Ast.ExprVar + { Ast.name = "a"; + span = + { Span.start_pos = + { Span.line = 11; col = 10; + offset = 177 }; + end_pos = + { Span.line = 11; col = 12; + offset = 179 }; + file = "test/golden/binary_ops.affine" + } + }), + Ast.OpBitXor, + (Ast.ExprVar + { Ast.name = "b"; + span = + { Span.start_pos = + { Span.line = 11; col = 14; + offset = 181 }; + end_pos = + { Span.line = 11; col = 16; + offset = 183 }; + file = "test/golden/binary_ops.affine" + } + }) + )) + )), + Ast.OpShl, + (Ast.ExprLit + (Ast.LitInt (2, + { Span.start_pos = + { Span.line = 11; col = 19; offset = 186 }; + end_pos = + { Span.line = 11; col = 22; offset = 189 }; + file = "test/golden/binary_ops.affine" } + ))) + )), + Ast.OpShr, + (Ast.ExprLit + (Ast.LitInt (1, + { Span.start_pos = + { Span.line = 11; col = 24; offset = 191 }; + end_pos = + { Span.line = 11; col = 27; offset = 194 }; + file = "test/golden/binary_ops.affine" } + ))) + ))) + }) + }); + (Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = + { Ast.name = "test_bitwise_not"; + span = + { Span.start_pos = { Span.line = 14; col = 1; offset = 199 }; + end_pos = { Span.line = 14; col = 4; offset = 202 }; + file = "test/golden/binary_ops.affine" } + }; + fd_type_params = []; + fd_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = + { Ast.name = "n"; + span = + { Span.start_pos = { Span.line = 14; col = 20; offset = 218 }; + end_pos = { Span.line = 14; col = 21; offset = 219 }; + file = "test/golden/binary_ops.affine" } + }; + p_ty = + (Ast.TyCon + { Ast.name = "Int"; + span = + { Span.start_pos = + { Span.line = 14; col = 22; offset = 220 }; + end_pos = { Span.line = 14; col = 24; offset = 222 }; + file = "test/golden/binary_ops.affine" } + }) + } + ]; + fd_ret_ty = + (Some (Ast.TyCon + { Ast.name = "Int"; + span = + { Span.start_pos = + { Span.line = 14; col = 29; offset = 227 }; + end_pos = { Span.line = 14; col = 32; offset = 230 }; + file = "test/golden/binary_ops.affine" } + })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprUnary (Ast.OpBitNot, + (Ast.ExprVar + { Ast.name = "n"; + span = + { Span.start_pos = + { Span.line = 15; col = 3; offset = 238 }; + end_pos = + { Span.line = 15; col = 4; offset = 239 }; + file = "test/golden/binary_ops.affine" } + }) + ))) + }) }) ] } diff --git a/test/test_e2e.ml b/test/test_e2e.ml index 4e8677ba..6dc1e3f3 100644 --- a/test/test_e2e.ml +++ b/test/test_e2e.ml @@ -585,6 +585,11 @@ let linear_arrow_tests = [ 3. The binary starts with a valid WASM magic number *) +let test_wasm_bitwise () = + match run_wasm_pipeline (fixture "bitwise.affine") with + | Error msg -> Alcotest.fail msg + | Ok _wasm_mod -> () + let test_wasm_arithmetic () = match run_wasm_pipeline (fixture "arithmetic.affine") with | Error msg -> Alcotest.fail msg @@ -640,6 +645,7 @@ let test_wasm_lambda () = | Ok _wasm_mod -> () let wasm_tests = [ + Alcotest.test_case "bitwise codegen" `Quick test_wasm_bitwise; Alcotest.test_case "arithmetic codegen" `Quick test_wasm_arithmetic; Alcotest.test_case "simple program" `Quick test_wasm_simple; Alcotest.test_case "write binary" `Quick test_wasm_write_binary; @@ -657,6 +663,21 @@ let wasm_tests = [ 3. Function signatures map correctly *) +let test_julia_bitwise () = + match parse_fixture (fixture "bitwise.affine") with + | Error msg -> Alcotest.fail msg + | Ok prog -> + match resolve_program prog with + | Error msg -> Alcotest.fail msg + | Ok (ctx, _) -> + (match julia_codegen prog ctx.symbols with + | Error msg -> Alcotest.fail msg + | Ok code -> + (* Verify it contains Julia bitwise ops *) + Alcotest.(check bool) "contains &" true (String.contains code '&'); + Alcotest.(check bool) "contains |" true (String.contains code '|'); + Alcotest.(check bool) "contains ~" true (String.contains code '~')) + let test_julia_arithmetic () = match run_julia_pipeline (fixture "arithmetic.affine") with | Error msg -> Alcotest.fail msg @@ -708,6 +729,7 @@ let test_julia_full_pipeline () = (String.length code > 0) let julia_tests = [ + Alcotest.test_case "bitwise codegen" `Quick test_julia_bitwise; Alcotest.test_case "arithmetic codegen" `Quick test_julia_arithmetic; Alcotest.test_case "simple program" `Quick test_julia_simple; Alcotest.test_case "type mapping" `Quick test_julia_type_mapping; @@ -729,6 +751,14 @@ let test_interp_simple () = | Error msg -> Alcotest.fail msg | Ok _env -> () +let test_interp_bitwise () = + match parse_fixture (fixture "bitwise.affine") with + | Error msg -> Alcotest.fail msg + | Ok prog -> + (match Interp.eval_program prog with + | Ok _env -> () + | Error e -> Alcotest.fail (Value.show_eval_error e)) + let test_interp_arithmetic () = match run_interp_pipeline (fixture "arithmetic.affine") with | Error msg -> Alcotest.fail msg @@ -746,6 +776,7 @@ let test_interp_full_pipeline () = let interp_tests = [ Alcotest.test_case "simple evaluation" `Quick test_interp_simple; + Alcotest.test_case "bitwise" `Quick test_interp_bitwise; Alcotest.test_case "arithmetic" `Quick test_interp_arithmetic; Alcotest.test_case "lambda" `Quick test_interp_lambda; Alcotest.test_case "full pipeline" `Quick test_interp_full_pipeline; @@ -760,6 +791,14 @@ let interp_tests = [ 2. Optimization preserves semantics (same AST shape for non-constant exprs) *) +let test_opt_bitwise () = + match parse_fixture (fixture "bitwise.affine") with + | Error msg -> Alcotest.fail msg + | Ok prog -> + let _optimized = Opt.fold_constants_program prog in + (* bitwise_constant_fold should be reduced *) + () + let test_opt_constant_folding () = match parse_fixture (fixture "arithmetic.affine") with | Error msg -> Alcotest.fail msg @@ -786,6 +825,7 @@ let test_opt_preserves_semantics () = (Value.show_eval_error e))) let optimizer_tests = [ + Alcotest.test_case "bitwise folding" `Quick test_opt_bitwise; Alcotest.test_case "constant folding" `Quick test_opt_constant_folding; Alcotest.test_case "preserves semantics" `Quick test_opt_preserves_semantics; ] diff --git a/tests/codegen/README.md b/tests/codegen/README.md index c6c24ead..60dd172f 100644 --- a/tests/codegen/README.md +++ b/tests/codegen/README.md @@ -11,7 +11,7 @@ From the repo root: ``` The script: -- compiles every `tests/codegen/*.as` file to `tests/codegen/*.wasm` +- compiles every `tests/codegen/*.affine` file to `tests/codegen/*.wasm` - runs any `tests/codegen/*.mjs` harnesses ## Notes