diff --git a/CHANGELOG.md b/CHANGELOG.md index 87933d5cf9da7b6feffdf8fc5791dcc371e034f5..cfd8175aba330b45c6fc9a28a89615b87102ad39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,15 @@ - feat(sema): coersion: numeric\_literal -> float - feat(sema): prevent passing non-runtime compatible types like `type` to opaque functions +- feat(sema): properly validate and coerce binary operations +- feat(sema): string concat with `+` - feat(transform): lhs of assignments now validated - feat(transform): mark useless discard - feat(transform): parse string escapes properly - feat(link): print string escapes properly - feat: implement the `comptime` keyword (many comptime ops are not implemented) - feat: add `@jsNew` to create JavaScript `new` expressions +- fix(parse): top-level if statements previously required semicolons - fix(sema): properly coerce assignments - fix(transform): empty block generated in top level code tripped sema assertion - fix(link): emit parenthesis to preserve operator precedence diff --git a/build b/build index 3c2e1a79d588ca1c4ba3e0d7a5a6d723e87f1ab7..c45b3fcbc8c33908e4212dfb68754946035478df 100755 --- a/build +++ b/build @@ -1,6 +1,6 @@ #!/bin/sh # This script will build the compiler on any POSIX system. -# It automatically installs a Zig compiler to ./zig-cache +# It automatically installs a Zig compiler to ./.zig-cache # if needed. The output will be in ./zig-out/bin. # # `curl` and `tar` is required in order to download and @@ -11,7 +11,7 @@ cd -- "$(dirname -- "$0")" ZIG_VERSION=$(grep .minimum_zig_version build.zig.zon \ | cut -d '"' -f 2) -ZIG="zig-cache/zig/$ZIG_VERSION/zig" +ZIG=".zig-cache/zig/$ZIG_VERSION/zig" if [ ! -f "$ZIG" ]; then OS=$(uname -s | tr '[:upper:]' '[:lower:]') if [ "$OS" = "darwin" ]; then @@ -40,12 +40,12 @@ if [ ! -f "$ZIG" ]; then exit 1 fi - mkdir -p zig-cache/zig/$ZIG_VERSION + mkdir -p .zig-cache/zig/$ZIG_VERSION curl -fsSL --retry 3 --retry-delay 3 $URL \ - | tar -xJ --strip-components=1 -C zig-cache/zig/$ZIG_VERSION + | tar -xJ --strip-components=1 -C .zig-cache/zig/$ZIG_VERSION if [ "$($ZIG version)" != "$ZIG_VERSION" ]; then echo "Zig failed to install." - rm -rf zig-cache/zig/$ZIG_VERSION + rm -rf .zig-cache/zig/$ZIG_VERSION exit 1 fi fi diff --git a/build.cmd b/build.cmd index 07879cf6f0c99cdada497c0caa5fc6fb491beafb..f3c5fff0671365fad1e5a846bcf2ddf5bf593f60 100644 --- a/build.cmd +++ b/build.cmd @@ -1,7 +1,7 @@ @ECHO OFF @REM This script will build the compiler on any Windows system -@REM It automatically installs a Zig compiler to ./zig-cache -@REM The output will be in ./zig-out/bin +@REM It automatically installs a Zig compiler to \/.zig-cache +@REM The output will be in .\zig-out\bin SET ROOT=%~dp0 FOR /F "tokens=3 delims=, " %%i IN ( @@ -9,9 +9,9 @@ FOR /F "tokens=3 delims=, " %%i IN ( "%ROOT%build.zig.zon"' ) DO SET ZIG_VERSION=%%i SET ZIG_VERSION=%ZIG_VERSION:~1,-1% -IF NOT EXIST "%ROOT%zig-cache\zig\%ZIG_VERSION%\zig.exe" GOTO :find_powershell +IF NOT EXIST "%ROOT%.zig-cache\zig\%ZIG_VERSION%\zig.exe" GOTO :find_powershell :build -"%ROOT%zig-cache\zig\%ZIG_VERSION%\zig.exe" build %* +"%ROOT%.zig-cache\zig\%ZIG_VERSION%\zig.exe" build %* EXIT /b %ERRORLEVEL% :find_powershell FOR %%i IN (pwsh powershell) DO WHERE %%i /Q && SET PS=%%i && GOTO :download @@ -20,10 +20,10 @@ EXIT /b 1 :download SET ZIG_ARCH=x86_64 SET URI=https://ziglang.org/builds/zig-windows-%ZIG_ARCH%-%ZIG_VERSION%.zip -SET X=$null = New-Item -ItemType Directory -Force -Path zig-cache\zig -SET X=%X%;Invoke-RestMethod -Uri %URI% -OutFile zig-cache/zig-%ZIG_VERSION%.zip -SET X=%X%;Expand-Archive zig-cache/zig-%ZIG_VERSION%.zip -DestinationPath zig-cache/zig -SET X=%X%;Remove-Item zig-cache/zig-%ZIG_VERSION%.zip +SET X=$null = New-Item -ItemType Directory -Force -Path .zig-cache\zig +SET X=%X%;Invoke-RestMethod -Uri %URI% -OutFile .zig-cache/zig-%ZIG_VERSION%.zip +SET X=%X%;Expand-Archive .zig-cache/zig-%ZIG_VERSION%.zip -DestinationPath .zig-cache/zig +SET X=%X%;Remove-Item .zig-cache/zig-%ZIG_VERSION%.zip %PS% -NoProfile -Command "%X%" -MOVE zig-cache\zig\zig-windows-%ZIG_ARCH%-%ZIG_VERSION% zig-cache\zig\%ZIG_VERSION% >NUL +MOVE .zig-cache\zig\zig-windows-%ZIG_ARCH%-%ZIG_VERSION% .zig-cache\zig\%ZIG_VERSION% >NUL GOTO :build diff --git a/build.zig b/build.zig index 369cb7d7e9f0aeca14d21f02fe1f3ae197e591c5..d6729aaeef876a735eb11a9479a9dbe85e04f4ab 100644 --- a/build.zig +++ b/build.zig @@ -59,22 +59,22 @@ pub fn build(b: *std.Build) void { } // Language Reference Generator - const langref_generator = b.addExecutable(.{ - .name = "generate-langref", - .root_source_file = b.path("src/langref/generate-langref.zig"), + const manual_generator = b.addExecutable(.{ + .name = "generate-man", + .root_source_file = b.path("src/manual/generate-man.zig"), .target = native_target, .optimize = .Debug, .link_libc = true, }); - langref_generator.root_module.addImport("build_options", native_debug.opts_mod); - langref_generator.root_module.addImport("core", native_debug.core); - const langref_run = b.addRunArtifact(langref_generator); - langref_run.addFileArg(native_debug.compiler.getEmittedBin()); - langref_run.addFileArg(b.path("doc/language-reference.md")); - langref_run.addFileArg(b.path("src/langref/template.html")); - const langref_out = langref_run.addOutputFileArg("language-reference.html"); - const install_langref = b.addInstallFile(langref_out, "doc/README.html"); - b.step("langref", "build language reference").dependOn(&install_langref.step); + manual_generator.root_module.addImport("build_options", native_debug.opts_mod); + manual_generator.root_module.addImport("core", native_debug.core); + const manual_run = b.addRunArtifact(manual_generator); + manual_run.addFileArg(native_debug.compiler.getEmittedBin()); + manual_run.addFileArg(b.path("doc/language-manual.md")); + manual_run.addFileArg(b.path("src/manual/template.html")); + const manual_out = manual_run.addOutputFileArg("language-reference.html"); + const install_manual = b.addInstallFile(manual_out, "doc/README.html"); + b.step("manual", "build language reference").dependOn(&install_manual.step); // const wasm_api = b.addExecutable(.{ // .name = "compiler", @@ -137,8 +137,8 @@ pub fn build(b: *std.Build) void { ).step); } - package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "release/CHANGELOG.md").step); - package.dependOn(&b.addInstallFile(langref_out, "release/README.html").step); + package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "package/CHANGELOG.md").step); + package.dependOn(&b.addInstallFile(manual_out, "package/README.html").step); } } diff --git a/demo.todo b/demo.todo new file mode 100644 index 0000000000000000000000000000000000000000..0a2e8fb7b365173c38ff8aa3b1263685f9b2dc96 --- /dev/null +++ b/demo.todo @@ -0,0 +1,10 @@ +fn something(a: int) -> bool { + if a == 0 { + return false; + } + + a > 100 +} + +let print = @js("console").log; +print(something(0xFF), something(0)); diff --git a/doc/language-manual.md b/doc/language-manual.md new file mode 100644 index 0000000000000000000000000000000000000000..3194dda9d0a3482e8a479808be7cfc9d488e50cc --- /dev/null +++ b/doc/language-manual.md @@ -0,0 +1,160 @@ +# New Language Project + +This is a high-level, general purpose programming language. + +Key goals of this project are: + +- **Ease of Use**: Use of type inference keeps code concise and easy to read and write. +- **Fast Iteration**: The compiler is designed for quick incremental compilation and hot module reloading. (note: incremental is not yet available) +- TODO: some more + +It primarily focuses on merging some great points of Rust, Zig, and JavaScript's language designs: + +- **Rust** - Expressive control flow that is easy to write. +- **Zig** - Execute any code at compile time, everything is an expression. +- **JavaScript** - Garbage collected values with a versitile type system. + +## Standard Library + +Currently, there is no standard library. One will be introduced later. + +## Hello World + +```lang:run +let print = @js("console").log; +print("Hello, world!"); +``` + +The `print` function is implemented by getting an external reference to the `console` JavaScript global, and then retriving the `log` property from it. + +## Primative Data Types + +| name | ts equivilent | description | +| ---------- | --------------------- | ---------------------------------------------- | +| `int` | `number` | a number in signed 32-bit range | +| `float` | `number` | a 64-bit floating point number | +| `bool` | `boolean` | either `true` or `false` | +| `string` | `string` | a sequence of text characters | +| `void` | `undefined` or `null` | detectable lack of a value | +| `opaque` | `unknown` or `any` | could be any value | +| `noreturn` | `never` | control flow prevents this value from existing | + +> **note**: `bigint` is planned but not added + +## Built-in Constants + +In addition to the above types being built-in constants, the following are available. + +| name | description | +| ------- | ------------------------------------- | +| `true` | boolean true (result of `1 == 1`) | +| `false` | boolean false (result of `1 == 2`) | +| `std` | handle to the standard library module | + +These constants are reserved names, and it is an error to re-declare them. + +```lang:build +let true = 1 == 0; +``` + +## Variables + +There are two types of variables, `let` and `var`. + +`let` bindings cannot be overridden, and should be thought of as aliases of the expression within. Referring to these identifiers is equivilent to inlining it's where it was used, aside from side effect duplication. + +```lang:run +let hello: int = 1; +let world = hello + 2; +let hello = hello * world; + +@js("console").log(hello); +``` + +`var` bindings create mutable memory locations at runtime. This means they can be re-assigned to: + +```lang:run +var message = "first message"; + +if @js("Math").random() > 0.5 { + message = "second message"; +} + +@js("console").log(message); +``` + +Using `let` is preferred whenever possible, as it will lead to more readable code, and the compiler is able to optimize it better. The above can be rewritten as: + +```lang:run +let message = if @js("Math").random() > 0.5 { + "second message" +} else { + "first message" +}; + +@js("console").log(message); +``` + +It should be noted that numeric literals are a comptime-only data type, meaning it cannot be assigned to a `var` without an explicit type annotation. Since `int` is a strict subset of valid `float`s, the coersion will implicitly be a float, but a compiler warning will be emitted. + +```lang +var number = 0; // `number` is a `float`, but not explicit +var number: int = 0; // `number` is `int` +var number: float = 0; // `number` is `float` +``` + +## Functions + +Functions are declared with the `fn` keyword. Parameter types must be annotated. The function body is a block, which may use implicit return to return. + +```lang:run +fn add(a: int, b: int) -> int { + a + b +} + +let print = @js("console").log; +print("Sum of 2 and 7:", add(2, 7)); +``` + +> **Bug**: Return types are currently required. Omitting the `-> type` syntax currently makes the return type `void`, but this will be changed in the future to be an inferred return type. + +The `return` expression can be used within functions to return early. + +```lang:run +fn something(a: int) -> bool { + if a == 0 { + return false; + } + + a > 100 +} + +let print = @js("console").log; +print(something(0xFF), something(0)); +``` + +## Number Literals + +TODO + +## String Literals + +TODO + +## JavaScript Interop + +A global variable handle can be retrived with `@js`. We've seen this in the hello world. The type of this value is a special type called `opaque`, which disables type-checking. + +> **Note**: A future update will improve on the type-safety of `opaque` + +Many JS-exclusive actions are exposed via built-in functions, such as using `@jsNew` for calling constructors. + +```lang:run +let print = @js("console").log; + +fn currentYear() -> int { + @jsNew(@js("Date")).getFullYear() +} + +print(currentYear()); +``` diff --git a/doc/language-reference.md b/doc/language-reference.md deleted file mode 100644 index abc1ebb3cb5c98d9b25f568f362dcf66c590d51f..0000000000000000000000000000000000000000 --- a/doc/language-reference.md +++ /dev/null @@ -1,157 +0,0 @@ -# New Language Project - -This is a high-level, general purpose programming language. - -Key goals of this project are: - -- **Ease of Use**: Use of type inference keeps code concise and easy to read and write. -- **Fast Iteration**: The compiler is designed for quick incremental compilation and hot module reloading. -- TODO: some more - -It primarily focuses on merging some great points of Rust, Zig, and JavaScript's language designs: - -- **Rust** - Expressive control flow that is easy to write. -- **Zig** - Execute any code at compile time, everything is an expression. -- **JavaScript** - Garbage collected values with a versitile type system. - -## Standard Library - -Currently, there is no standard library. One will be introduced later. - -## Hello World - -```lang:run -let print = @js("console").log; -print("Hello, world!"); -``` - -The `print` function is implemented by getting an external reference to the `console` JavaScript global, and then retriving the `log` property from it. - -## Primative Data Types - -| name | ts equivilent | description | -| ---------- | --------------------- | ---------------------------------------------- | -| `int` | `number` | a number in signed 32-bit range | -| `float` | `number` | a 64-bit floating point number | -| `bool` | `boolean` | either `true` or `false` | -| `string` | `string` | a sequence of text characters | -| `void` | `undefined` or `null` | detectable lack of a value | -| `noreturn` | `never` | control flow prevents this value from existing | - -> **note**: `bigint` is planned but not added - -## Built-in Constants - -In addition to the above types being built-in constants, the following are available. - -| name | description | -| ------- | ------------------------------------- | -| `true` | boolean true (result of `1 == 1`) | -| `false` | boolean false (result of `1 == 2`) | -| `std` | handle to the standard library module | - -These constants are reserved names, and it is an error to re-declare them. - -```lang:build -let true = 1 == 0; -``` - -## Variables - -There are two types of variables, `let` and `var`. - -`let` bindings cannot be overridden, and should be thought of as aliases of the expression within. Referring to these identifiers is equivilent to inlining it's where it was used, aside from side effect duplication. - -```lang:run -let hello: int = 1; -let world = hello + 2; -let hello = hello * world; - -@js("console").log(hello); -``` - -`var` bindings create mutable memory locations at runtime. This means they can be re-assigned to: - -```lang:run -var message = "first message"; - -if @js("Math").random() > 0.5 { - message = "second message"; -} - -@js("console").log(message); -``` - -Using `let` is preferred whenever possible, as it will lead to more readable code, and the compiler is able to optimize it better. The above can be rewritten as: - -```lang:run -let message = if @js("Math").random() > 0.5 { - "second message" -} else { - "first message" -}; - -@js("console").log(message); -``` - -It should be noted that numeric literals are a comptime-only data type, meaning it cannot be assigned to a `var` without an explicit type annotation. Since `int` is a strict subset of valid `float`s, the coersion will implicitly be a float, but a compiler warning will be emitted. - -```lang -var number = 0; // `number` is a `float`, but not explicit -var number: int = 0; // `number` is `int` -var number: float = 0; // `number` is `float` -``` - -## Functions - -Functions are declared with the `fn` keyword. Parameter types must be annotated. The function body is a block, which may use implicit return to return. - -```lang:run -fn add(a: int, b: int) -> int { - a + b -} - -let print = @js("console").log; -print("Sum of 2 and 7" + add(2, 7)); -``` - -> **Bug**: Return types are currently required. Omitting the `-> type` syntax currently makes the return type `void`, but this will be changed in the future to be an inferred return type. - -The `return` expression can be used within functions to return early. - -```lang:run -fn something(a: int) -> bool { - if a == 0 { - return false; - } - - a > 100 -} - -let print = @js("console").log; -print(something(0xFF), something(0)); -``` - -## Number Literals - -TODO - -## String Literals - -TODO - -## JavaScript Interop - -A global variable handle can be retrived with `@js`. We've seen this in the hello world. The plan is to eventually replace this construct with something else, but implementing a function like this is extremely trivial for the compiler. - -Many JS-exclusive actions are exposed via built-in functions, such as using `@jsNew` for calling constructors. - -```lang:run -let print = @js("console").log; - -fn currentYear() -> int { - @jsNew(@js("Date")).getFullYear() -} - -print(currentYear()); -``` diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index 208fb755e435af2ceb8b49e8ed59881cd7358056..92bac50ea2f5b886022525f8ee2c963ecae801c5 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -288,6 +288,14 @@ pub const Value = enum(u32) { pub fn toFloat(val: Value, ip: *const InternPool) ?f64 { return switch (val.valueTag(ip)) { inline .comptime_float, .float => |tag| val.valueData(ip, tag).get(ip).decode(), + .int32 => @floatFromInt(val.valueData(ip, .int32)), + else => null, + }; + } + + pub fn toInt32(val: Value, ip: *const InternPool) ?i32 { + return switch (val.valueTag(ip)) { + .int32 => val.valueData(ip, .int32), else => null, }; } diff --git a/src/core/JsLink.zig b/src/core/JsLink.zig index 33db47c68d495888b4f0d5e90871f5e64f94430c..7c9975be09458410f563d5ba057867727df1e40e 100644 --- a/src/core/JsLink.zig +++ b/src/core/JsLink.zig @@ -193,10 +193,10 @@ fn printBlockContents( for (list.slice(tree)) |stmt| { try link.printExpr(tree, stmt, writer); switch (tree.nodeTag(stmt)) { - else => try writer.writeAll(";"), .stmt_block, .stmt_if, => {}, + else => try writer.writeAll(";"), } try writer.writeAll("\n"); } diff --git a/src/core/Parse.zig b/src/core/Parse.zig index 2fb2433bc2b0138d1da8f3ecc75211b3c0b5064c..b374d560d6570551f13126969a85e38916915e93 100644 --- a/src/core/Parse.zig +++ b/src/core/Parse.zig @@ -245,8 +245,20 @@ fn parseModuleMembers(p: *Parse) Error!SubList { return p.failUnexpectedToken(.statement); } + const first_tag = p.currentTag(); + try p.scratch.append(p.gpa, try p.parseExpression(true)); - _ = try p.expect(.semi); + + switch (first_tag) { + // some control flow and blocks are allowed to not have a + // trailing semicolons. this is a stylistic choice. + .kw_if, + .start_brace, + => {}, + + // TODO: make semicolons optional, but emit a warning + else => _ = try p.expect(.semi), + } }, }; @@ -423,9 +435,7 @@ fn parseBlockContents(p: *Parse) Error!struct { Node.Tag, SubList } { if (p.currentTag() == .end_brace) break .block_implicit_return; }, - // TODO: make kw_break and kw_return REQUIRE a semicolon, as - // they are noreturn statements, doing implicit break on - // them makes no sense + // TODO: make semicolons optional, but emit a warning else => { if (p.eatToken(.semi) == null) { @@ -456,13 +466,13 @@ fn parseVarDecl(p: *Parse) Error!NodeIndex { _ = try p.expect(.id); const data_type: OptionalNodeIndex = if (p.eatToken(.colon)) |_| - optionalNodeIndex(try p.parseExpression(false)) + optionalNodeIndex(try p.tryParseExpression(false)) else .none; _ = try p.expect(.equal); - const data_init = try p.parseExpression(true); + const data_init = try p.tryParseExpression(true); _ = try p.expect(.semi); @@ -711,6 +721,13 @@ fn parseExpression(p: *Parse, allow_assign_operators: bool) Error!NodeIndex { return expr; } +fn tryParseExpression(p: *Parse, allow_assign_operators: bool) Error!NodeIndex { + return if (p.isExpressionStart()) + p.parseExpression(allow_assign_operators) + else + p.failUnexpectedToken(.expression); +} + fn parseExpressionPart(p: *Parse) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -1040,8 +1057,14 @@ fn fail(p: *Parse, ast_error: AstError) Error { } fn failUnexpectedToken(p: *Parse, kind: ExpectedContext) Error { + var got = p.fullToken(p.pos); + if (got.tag == .eof) { + // note: it is impossible to get a failure at an empty file, so subtraction is fine + const prev = p.fullToken(p.pos - 1); + got.offset = prev.offset + prev.len(p.source); + } return p.fail(.{ .unexpected_context = .{ - .got = p.fullToken(p.pos), + .got = got, .expected = kind, } }); } diff --git a/src/core/Report.zig b/src/core/Report.zig index 808b104619c990956d700d493c006358073c6b30..5cfc4ec59718f2026b2d50a6c3253961e378b679 100644 --- a/src/core/Report.zig +++ b/src/core/Report.zig @@ -247,10 +247,24 @@ pub const Err = union(enum) { node: NodeIndex, fn build(data: @This(), ctx: *MsgContext) !void { + try ctx.msgNode(data.node, "invalid assignment target", .{}); + } + }, + bin_op_mismatch: struct { + src: LazyNode, + lhs: Type, + rhs: Type, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); try ctx.msgNode( - data.node, - "invalid assignment target", - .{}, + node, + "\"{s}\" operator is not compatible with types \"{}\" and \"{}\"", + .{ + ctx.ast.tokenSlice(ctx.ast.nodeToken(node)), + data.lhs.displayName(&ctx.comp.intern_pool), + data.rhs.displayName(&ctx.comp.intern_pool), + }, ); } }, @@ -458,7 +472,13 @@ pub fn formatItem( const file = try comp.getFile(item.url.slice(report)); const need_load_ast = !(item_type == .@"error" and item.data.@"error" == .ast); - const ast_data = if (need_load_ast) try file.getAst(comp) else undefined; + const ast_data = if (need_load_ast) + try file.getAst(comp) + else brk: { + var ast: Ast = undefined; + ast.utf8_source = (try file.getSource(comp.gpa)).code; + break :brk ast; + }; var ctx: MsgContext = .{ .arena = alloc, @@ -700,13 +720,12 @@ pub const Highlight = union(enum) { .byte = token.offset, }; }, - .token_stored => |token| loc: { const location = Tokenizer.tokenLocation(ast.utf8_source, 0, token.offset); break :loc .{ .loc = location, - .slice = token.slice(ast.utf8_source), + .slice = if (token.tag != .eof) token.slice(ast.utf8_source) else "\n", .byte = token.offset, }; }, diff --git a/src/core/Sema.zig b/src/core/Sema.zig index 2ad726b477901e72b2039ac9cc71577a6e9960d6..e6703ac99863aeeada652c1f546b5de985068290 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -315,28 +315,30 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .eq, => |tag| { const bin: Teg.Inst.Bin = s.code.instPayload(tag, in); + const op = @field(BinaryOp, @tagName(tag)); const lhs = try s.resolveRefRuntime(bin.lhs); const rhs = try s.resolveRefRuntime(bin.rhs); - if (lhs.kind == .value and rhs.kind == .value) { + const left_ty = s.typeOfRef(lhs); + const right_ty = s.typeOfRef(rhs); + + const ty = try s.binTypeResolution(in, op, left_ty, right_ty); + + const left = try s.coerceRef(in.op(.lhs), lhs, ty); + const right = try s.coerceRef(in.op(.rhs), lhs, ty); + + if (left.kind == .value and right.kind == .value) { return (try s.comptimeBin( - @field(BinaryOp, @tagName(tag)), - lhs.decode().value, - rhs.decode().value, - )); + op, + left.decode().value, + right.decode().value, + )).toAir(); } - const left_type = s.typeOfRef(lhs); - _ = left_type; // autofix - const right_type = s.typeOfRef(rhs); - _ = right_type; // autofix - - // TODO: validate type for operation - return s.addInstRef(tag.airTag(), .{ - .lhs = lhs, - .rhs = rhs, + .lhs = left, + .rhs = right, }); }, inline .negate, .bool_not => |tag| { @@ -881,35 +883,133 @@ const BinaryOp = enum { eq, }; -fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Air.Inst.Ref { +fn binTypeResolution(s: *Sema, src: Teg.Inst.Index, op: BinaryOp, lhs: Type, rhs: Type) !Type { + if (lhs == .@"opaque") return rhs; + if (rhs == .@"opaque") return rhs; + + switch (lhs) { + .string => if (op == .add) switch (rhs) { + .string => return .string, + else => {}, + }, + .numeric_literal => switch (rhs) { + .int32, + .float, + .numeric_literal, + => return rhs, + else => {}, + }, + .int32 => switch (rhs) { + .int32 => return .int32, + .numeric_literal => return .int32, + else => {}, + }, + .float => switch (rhs) { + .int32 => return .float, + .numeric_literal => return .float, + else => {}, + }, + else => {}, + } + + return s.fail(.bin_op_mismatch, .{ + .src = s.nodeLoc(src, .main_node), + .lhs = lhs, + .rhs = rhs, + }); +} + +fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Value { + std.debug.print("{} {s} {}\n", .{ + lhs.dump(s.ip), + @tagName(op), + rhs.dump(s.ip), + }); + + const left_ty = lhs.typeOf(s.ip); + assert(rhs.typeOf(s.ip) == left_ty); + switch (op) { - .add => { - const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck / string add"); - const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck / string add"); - return (try s.ip.get(s.gpa, .{ - .float = l + r, - })).toAir(); + .add => switch (left_ty) { + .numeric_literal, .float => { + const l = lhs.toFloat(s.ip) orelse + unreachable; // must be coerced + const r = rhs.toFloat(s.ip) orelse + unreachable; // must be coerced + + const result = l + r; + + return try s.makeValue(if (left_ty == .numeric_literal) + .{ .comptime_float = result } + else + .{ .float = result }); + }, + .int32 => { + const l = lhs.toInt32(s.ip) orelse + unreachable; // must be coerced + const r = rhs.toInt32(s.ip) orelse + unreachable; // must be coerced + + const result = std.math.add(i32, l, r) catch + @panic("TODO: out of range"); + + return try s.makeValue(.{ .int32 = result }); + }, + .string => { + const l = lhs.asString(s.ip).toSlice(s.ip); + const r = rhs.asString(s.ip).toSlice(s.ip); + var sfa = std.heap.stackFallback(4096, s.arena); + const alloc = sfa.get(); + const joined = try std.mem.concat(alloc, u8, &.{ l, r }); + defer alloc.free(joined); + return try s.makeValue(.{ .string = joined }); + }, + else => unreachable, }, - inline .sub, .mul, .div => |tag| { - const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck"); - const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck"); - - const result = switch (tag) { - .sub => l - r, - .mul => l * r, - .div => l / r, - else => comptime unreachable, - }; - - return (try s.ip.get(s.gpa, .{ - .float = result, - })).toAir(); + inline .sub, .mul, .div => |tag| switch (left_ty) { + .numeric_literal, .float => { + const l = lhs.toFloat(s.ip) orelse + unreachable; // must be coerced + const r = rhs.toFloat(s.ip) orelse + unreachable; // must be coerced + + const result = switch (tag) { + .sub => l - r, + .mul => l * r, + .div => l / r, + else => comptime unreachable, + }; + + return try s.makeValue(if (left_ty == .numeric_literal) + .{ .comptime_float = result } + else + .{ .float = result }); + }, + .int32 => { + const l = lhs.toInt32(s.ip) orelse + unreachable; // must be coerced + const r = rhs.toInt32(s.ip) orelse + unreachable; // must be coerced + + const result = switch (tag) { + .sub => std.math.sub(i32, l, r), + .mul => std.math.mul(i32, l, r), + .div => return try s.makeValue(.{ .float = @as(f64, @floatFromInt(l)) / @as(f64, @floatFromInt(r)) }), + else => comptime unreachable, + } catch @panic("TODO: out of range"); + + return try s.makeValue(.{ .int32 = result }); + }, + else => unreachable, }, inline .lt, .gt, => |tag| { - const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck"); - const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck"); + const l = lhs.toFloat(s.ip) orelse + unreachable; // must be coerced + const r = rhs.toFloat(s.ip) orelse + unreachable; // must be coerced + const result = switch (tag) { .lt => l < r, .gt => l > r, @@ -918,17 +1018,15 @@ fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Air.Inst.Ref { return @as( InternPool.Value, if (result) .bool_true else .bool_false, - ).toAir(); + ); }, .eq, => { - const l = lhs.toFloat(s.ip) orelse @panic("TODO: typecheck / non-numeric eq"); - const r = rhs.toFloat(s.ip) orelse @panic("TODO: typecheck / non-numeric eq"); - const result = l == r; + const result = lhs == rhs; return @as( InternPool.Value, if (result) .bool_true else .bool_false, - ).toAir(); + ); }, } } diff --git a/src/core/Tokenizer.zig b/src/core/Tokenizer.zig index 72aa83aa5bf0262e2c7127f6e371ed8315301290..d0256f8552eefec40dbb6dd72f64ebeb727772a4 100644 --- a/src/core/Tokenizer.zig +++ b/src/core/Tokenizer.zig @@ -152,7 +152,6 @@ pub const Token = struct { .invalid => unreachable, // do not have a static lexeme - .eof, .id, .builtin_id, .number_int, @@ -163,6 +162,8 @@ pub const Token = struct { .module_doc_comment, => null, + .eof => unreachable, // always must be special cased + .@"and" => "&", .and_and => "&&", .bang => "!", diff --git a/src/langref/generate-langref.zig b/src/langref/generate-langref.zig deleted file mode 100644 index cab82334924c09e8528aaa57e0a860a7134822d1..0000000000000000000000000000000000000000 --- a/src/langref/generate-langref.zig +++ /dev/null @@ -1,364 +0,0 @@ -//! Language reference compiler. Converts markdown into HTML, using -//! the Tokenizer to highlight code, and the compiler and node.js to -//! show code results. Only ever built in Debug mode. We are not -//! smart about memory. Written at 3am on a Thursday. Enjoy -pub const std = @import("std"); -pub const core = @import("core"); - -const ArrayList = std.ArrayList; - -pub fn main() !void { - comptime std.debug.assert(@import("builtin").mode == .Debug); - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - const gpa = arena.allocator(); - - var args = try std.process.argsWithAllocator(gpa); - _ = args.next().?; - - const compiler_path = args.next().?; - const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); - const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); - const out_path = args.next().?; - - var output = std.ArrayList(u8).init(gpa); - defer output.deinit(); - try processMarkdown( - gpa, - markdown_src, - compiler_path, - output.writer(), - ); - - const applied_template = try std.mem.replaceOwned( - u8, - gpa, - template_src, - "{contents}", - output.items, - ); - defer gpa.free(applied_template); - - const out_file = try std.fs.createFileAbsolute(out_path, .{}); - defer out_file.close(); - try out_file.writeAll(applied_template); -} - -const BlockType = enum { - para, - h1, - h2, - h3, - code_run, - code_build, - code, - code_generic, - ul, - table, - blockquote, -}; - -const InlineType = enum { - bold, - code, -}; - -const MarkdownTokenizer = struct { - /// Remaining text - str: []const u8, - - fn skipWhitespace(t: *MarkdownTokenizer) void { - t.str = std.mem.trimLeft(u8, t.str, " \r\n"); - } - - fn advance(t: *MarkdownTokenizer, n: usize) []const u8 { - defer { - t.str = t.str[n..]; - } - return t.str[0..n]; - } - - fn advanceLine(t: *MarkdownTokenizer) []const u8 { - return std.mem.trim(u8, t.advanceLineRaw(), " \r\n"); - } - - fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 { - const n = std.mem.indexOfScalar(u8, t.str, '\n'); - const line = t.advance(if (n) |x| x + 1 else t.str.len); - return line; - } - - fn blockType(t: *MarkdownTokenizer) BlockType { - switch (t.str[0]) { - '#' => { - defer { - while (t.str[0] == '#') - _ = t.advance(1); - } - - if (t.str[1] != '#') return .h1; - if (t.str[2] != '#') return .h2; - if (t.str[3] != '#') return .h3; - @panic("headers nested too deep"); - }, - '>' => { - _ = t.advance(1); - return .blockquote; - }, - '|' => { - return .table; - }, - '`' => { - if (std.mem.startsWith(u8, t.str, "```")) { - defer _ = t.advanceLine(); - if (std.mem.startsWith(u8, t.str, "```lang:run")) - return .code_run; - if (std.mem.startsWith(u8, t.str, "```lang:build")) - return .code_build; - if (std.mem.startsWith(u8, t.str, "```lang")) - return .code; - return .code_generic; - } - return .para; - }, - '-' => { - return .ul; - }, - else => { - return .para; - }, - } - } -}; - -fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []const u8, writer: anytype) !void { - var t = MarkdownTokenizer{ .str = src }; - - t.skipWhitespace(); - while (t.str.len > 0) : (t.skipWhitespace()) { - switch (t.blockType()) { - .para, - => { - try writer.writeAll("
\n"); - - var para = ArrayList(u8).init(gpa); - try para.appendSlice(t.advanceLine()); - - var next_line = t.advanceLine(); - while (t.str.len > 0 and next_line.len > 0) { - try para.append(' '); - try para.appendSlice(next_line); - next_line = t.advanceLine(); - } - - try processInlineMarkdown(gpa, para.items, writer); - - try writer.writeAll("\n
\n"); - }, - .h1, - .h2, - .h3, - => |tag| { - const html_tag = @tagName(tag); - const name = t.advanceLine(); - try writer.print("<{0s}>{1s}{0s}>\n", .{ - html_tag, - name, - }); - }, - .code_run, - .code_build, - .code, - .code_generic, - => |tag| { - var code = ArrayList(u8).init(gpa); - - var next_line = t.advanceLineRaw(); - while (t.str.len > 0 and !std.mem.startsWith(u8, next_line, "```")) { - try code.appendSlice(next_line); - next_line = t.advanceLineRaw(); - } - - try writer.writeAll("");
- try writeTokenizedCode(gpa, code.items, writer);
- try writer.writeAll("");
-
- try code.append('\n');
-
- switch (tag) {
- .code_run,
- .code_build,
- => {
- var build = std.process.Child.init(&.{ compiler_path, "/dev/stdin" }, gpa);
- build.stdin_behavior = .Pipe;
- build.stdout_behavior = .Pipe;
- build.stderr_behavior = .Pipe;
- var env_map = try std.process.getEnvMap(gpa);
- defer env_map.deinit();
- try env_map.put("YES_COLOR", "1");
- build.env_map = &env_map;
- try build.spawn();
- try build.stdin.?.writeAll(code.items);
- var out = ArrayList(u8).init(gpa);
- var err = ArrayList(u8).init(gpa);
- try build.collectOutput(&out, &err, std.math.maxInt(usize));
- const result = try build.wait();
- _ = result;
- if (std.mem.trim(u8, err.items, " \n\r").len > 0) {
- try writer.writeAll("");
- try writeAnsi(gpa, err.items, writer);
- try writer.writeAll("");
- }
- if (std.mem.trim(u8, out.items, " \n\r").len > 0) {
- try writer.writeAll("");
- try writer.writeAll("// Compiled JavaScript Code");
- try writer.writeAll(out.items[std.mem.indexOfScalar(u8, out.items, '\n').?..]);
- try writer.writeAll("");
- }
- },
- else => {},
- }
- },
- .ul,
- => {
- try writer.writeAll("\n"); - - var para = ArrayList(u8).init(gpa); - try para.appendSlice(t.advanceLine()); - - var next_line = t.advanceLine(); - while (t.str.len > 0 and next_line.len > 0) { - try para.append(' '); - try para.appendSlice(next_line); - next_line = t.advanceLine(); - } - - try processInlineMarkdown(gpa, para.items, writer); - - try writer.writeAll("\n\n"); - }, - } - } -} - -pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } { - var pos: usize = 0; - while (true) : (pos += 1) { - pos = std.mem.indexOfAnyPos(u8, src, pos, "*`") orelse return null; - - switch (src[pos]) { - '*' => switch (src[pos + 1]) { - '*' => return .{ .bold, pos }, - else => continue, - }, - '`' => return .{ .code, pos }, - else => unreachable, - } - } -} - -fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytype) !void { - var bold = false; - var remain = src; - - while (nextInlineToken(remain)) |tok| { - try writer.writeAll(remain[0..tok[1]]); - remain = remain[tok[1]..]; - switch (tok[0]) { - .bold => { - bold = !bold; - try writer.writeAll(switch (bold) { - true => "", - false => "", - }); - remain = remain[2..]; - }, - .code => { - const end = std.mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block"); - const code = remain[1..end]; - try writer.writeAll("
");
- try writeTokenizedCode(gpa, code, writer);
- try writer.writeAll("");
- remain = remain[end + 1 ..];
- },
- }
- }
- try writer.writeAll(remain);
-
- if (bold) {
- std.debug.panic("Unterminated Bold in '{s}'", .{src});
- }
-}
-
-// TODO: Change this to use Tree-sitter, which would also make it very simple
-// to highlight JavaScript code in addition to the language.
-fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
- const code_z = try gpa.dupeZ(u8, code);
- defer gpa.free(code_z);
-
- var t = core.Tokenizer{ .code = code_z };
-
- t.skipWhitespace();
- var last: usize = 0;
- while (true) {
- t.skipWhitespace();
- const tok = t.next() catch {
- t.pos += 1;
- continue;
- } orelse break;
- try writer.writeAll(t.code[last..tok.offset]);
- try writer.print("{s}", .{
- @tagName(tok.tag),
- if (tok.tag.isKeyword()) " kw" else "",
- t.code[tok.offset..t.pos],
- });
- last = t.pos;
- }
-}
-
-fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
- var remain = code;
- var count: usize = 0;
- while (std.mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
- try writer.writeAll(remain[0..esc_i]);
- switch (remain[esc_i + 1]) {
- '[' => switch (remain[esc_i + 2]) {
- '0'...'9' => {
- const end = std.mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
- const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
- switch (int) {
- else => {
- try writer.print("", .{int});
- count += 1;
- },
- 0 => {
- for (0..count) |_| {
- try writer.writeAll("");
- }
- count = 0;
- },
- }
- remain = remain[end + 1 ..];
- },
- else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}),
- },
- else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}),
- }
- }
- _ = gpa;
-}
diff --git a/src/langref/template.html b/src/langref/template.html
deleted file mode 100644
index 765f49eff96554c42aa93dceec6f5dad82b9af1b..0000000000000000000000000000000000000000
--- a/src/langref/template.html
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
-
- \n"); + + var para = ArrayList(u8).init(gpa); + try para.appendSlice(t.advanceLine()); + + var next_line = t.advanceLine(); + while (t.str.len > 0 and next_line.len > 0) { + try para.append(' '); + try para.appendSlice(next_line); + next_line = t.advanceLine(); + } + + try processInlineMarkdown(gpa, para.items, writer); + + try writer.writeAll("\n
\n"); + }, + .h1, + .h2, + .h3, + => |tag| { + const html_tag = @tagName(tag); + const name = t.advanceLine(); + try writer.print("<{0s}>{1s}{0s}>\n", .{ + html_tag, + name, + }); + }, + .code_run, + .code_build, + .code, + .code_generic, + => |tag| { + var code = ArrayList(u8).init(gpa); + + var next_line = t.advanceLineRaw(); + while (t.str.len > 0 and !std.mem.startsWith(u8, next_line, "```")) { + try code.appendSlice(next_line); + next_line = t.advanceLineRaw(); + } + + try writer.writeAll("");
+ try writeTokenizedCode(gpa, code.items, writer);
+ try writer.writeAll("");
+
+ try code.append('\n');
+
+ switch (tag) {
+ .code_run,
+ .code_build,
+ => {
+ var build = std.process.Child.init(&.{ compiler_path, "/dev/stdin" }, gpa);
+ build.stdin_behavior = .Pipe;
+ build.stdout_behavior = .Pipe;
+ build.stderr_behavior = .Pipe;
+ var env_map = try std.process.getEnvMap(gpa);
+ defer env_map.deinit();
+ try env_map.put("YES_COLOR", "1");
+ build.env_map = &env_map;
+ try build.spawn();
+ try build.stdin.?.writeAll(code.items);
+ var out = ArrayList(u8).init(gpa);
+ var err = ArrayList(u8).init(gpa);
+ try build.collectOutput(&out, &err, std.math.maxInt(usize));
+ const result = try build.wait();
+ _ = result;
+ if (std.mem.trim(u8, err.items, " \n\r").len > 0) {
+ try writer.writeAll("");
+ try writeAnsi(gpa, err.items, writer);
+ try writer.writeAll("");
+ }
+ if (std.mem.trim(u8, out.items, " \n\r").len > 0) {
+ try writer.writeAll("");
+ try writer.writeAll("// Compiled JavaScript Code");
+ try writer.writeAll(out.items[std.mem.indexOfScalar(u8, out.items, '\n').?..]);
+ try writer.writeAll("");
+ }
+ },
+ else => {},
+ }
+ },
+ .ul,
+ => {
+ try writer.writeAll("| "); + try processInlineMarkdown(gpa, tok, writer); + try writer.writeAll(" | "); + } + try writer.writeAll("
|---|
| "); + try processInlineMarkdown(gpa, tok, writer); + try writer.writeAll(" | "); + } + try writer.writeAll("
\n"); + + var para = ArrayList(u8).init(gpa); + try para.appendSlice(t.advanceLine()); + + var next_line = t.advanceLine(); + while (t.str.len > 0 and next_line.len > 0) { + try para.append(' '); + try para.appendSlice(next_line); + next_line = t.advanceLine(); + } + + try processInlineMarkdown(gpa, para.items, writer); + + try writer.writeAll("\n\n"); + }, + } + } +} + +pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } { + var pos: usize = 0; + while (true) : (pos += 1) { + pos = std.mem.indexOfAnyPos(u8, src, pos, "*`") orelse return null; + + switch (src[pos]) { + '*' => switch (src[pos + 1]) { + '*' => return .{ .bold, pos }, + else => continue, + }, + '`' => return .{ .code, pos }, + else => unreachable, + } + } +} + +fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytype) !void { + var bold = false; + var remain = src; + + while (nextInlineToken(remain)) |tok| { + try writer.writeAll(remain[0..tok[1]]); + remain = remain[tok[1]..]; + switch (tok[0]) { + .bold => { + bold = !bold; + try writer.writeAll(switch (bold) { + true => "", + false => "", + }); + remain = remain[2..]; + }, + .code => { + const end = std.mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block"); + const code = remain[1..end]; + try writer.writeAll("
");
+ try writeTokenizedCode(gpa, code, writer);
+ try writer.writeAll("");
+ remain = remain[end + 1 ..];
+ },
+ }
+ }
+ try writer.writeAll(remain);
+
+ if (bold) {
+ std.debug.panic("Unterminated Bold in '{s}'", .{src});
+ }
+}
+
+// TODO: Change this to use Tree-sitter, which would also make it very simple
+// to highlight JavaScript code in addition to the language.
+fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
+ const code_z = try gpa.dupeZ(u8, code);
+ defer gpa.free(code_z);
+
+ var t = core.Tokenizer{ .code = code_z };
+
+ t.skipWhitespace();
+ var last: usize = 0;
+ while (true) {
+ t.skipWhitespace();
+ const tok = t.next() catch {
+ t.pos += 1;
+ continue;
+ } orelse break;
+ try writer.writeAll(t.code[last..tok.offset]);
+ try writer.print("{s}", .{
+ @tagName(tok.tag),
+ if (tok.tag.isKeyword()) " kw" else "",
+ t.code[tok.offset..t.pos],
+ });
+ last = t.pos;
+ }
+}
+
+fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
+ var remain = code;
+ var count: usize = 0;
+ while (std.mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
+ try writer.writeAll(remain[0..esc_i]);
+ switch (remain[esc_i + 1]) {
+ '[' => switch (remain[esc_i + 2]) {
+ '0'...'9' => {
+ const end = std.mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
+ const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
+ switch (int) {
+ else => {
+ try writer.print("", .{int});
+ count += 1;
+ },
+ 0 => {
+ for (0..count) |_| {
+ try writer.writeAll("");
+ }
+ count = 0;
+ },
+ }
+ remain = remain[end + 1 ..];
+ },
+ else => |x| std.debug.panic("Unknown esc + 2: {d}", .{x}),
+ },
+ else => |x| std.debug.panic("Unknown esc + 1: {d}", .{x}),
+ }
+ }
+ _ = gpa;
+}
diff --git a/src/manual/template.html b/src/manual/template.html
new file mode 100644
index 0000000000000000000000000000000000000000..a50003ed0cb3b3bd4c3aad9dbf685e7f22ddf201
--- /dev/null
+++ b/src/manual/template.html
@@ -0,0 +1,134 @@
+
+
+
+
+
+